Liam Goodacre
Liam Goodacre

Reputation: 423

Is it possible / advisable to return a range?

I'm using the ranges library to help filer data in my classes, like this:

class MyClass
{
public:
    MyClass(std::vector<int> v) : vec(v) {}

    std::vector<int> getEvens() const
    {
        auto evens = vec | ranges::views::filter([](int i) { return ! (i % 2); });
        return std::vector<int>(evens.begin(), evens.end());
    }

private:
    std::vector<int> vec;
};

In this case, a new vector is constructed in the getEvents() function. To save on this overhead, I'm wondering if it is possible / advisable to return the range directly from the function?

class MyClass
{
public:
    using RangeReturnType = ???;

    MyClass(std::vector<int> v) : vec(v) {}

    RangeReturnType getEvens() const
    {
        auto evens = vec | ranges::views::filter([](int i) { return ! (i % 2); });
        // ...
        return evens;
    }

private:
    std::vector<int> vec;
};

If it is possible, are there any lifetime considerations that I need to take into account?

I am also interested to know if it is possible / advisable to pass a range in as an argument, or to store it as a member variable. Or is the ranges library more intended for use within the scope of a single function?

Upvotes: 11

Views: 5148

Answers (6)

Ender-events
Ender-events

Reputation: 119

In c++23, you can use std::generator and co_yield std::ranges::elements_of

class MyClass
{
public:
    MyClass(std::vector<int> v) : vec(v) {}

    std::generator<int> getEvens() const
    {
        auto evens = vec | std::ranges::views::filter([](int i) { return ! (i % 2); });
        co_yield std::ranges::elements_of(evens);
    }

private:
    std::vector<int> vec;
};

int main() {
    MyClass mc{{1,2,3,4,5,6,7,8,9}};
    for (int i : mc.getEvens()) {
        std::cout << i << '\n';
    }
}

Working demo (GCC 13.1 without std::ranges::elements_of): https://godbolt.org/z/oehd59oEz

Upvotes: 6

Tom Huntington
Tom Huntington

Reputation: 3425

It is desirable to declare a function returning a range in a header and define it in a cpp file

  1. for compilation firewalls (compilation speed)
  2. stop the language server from going crazy
  3. for better factoring of the code

However, there are complications that make it not advisable: How to get type of a view?

  • If defining it in a header is fine, use auto
  • If performance is not a issue, I would recommend ranges::any_view
  • Otherwise I'd say it is not advisable.

Upvotes: 0

Liam Goodacre
Liam Goodacre

Reputation: 423

You can use ranges::any_view as a type erasure mechanism for any range or combination of ranges.

ranges::any_view<int> getEvens() const
{
    return vec | ranges::views::filter([](int i) { return ! (i % 2); });
}

I cannot see any equivalent of this in the STL ranges library; please edit the answer if you can.

EDIT: The problem with ranges::any_view is that it is very slow and inefficient. See https://github.com/ericniebler/range-v3/issues/714.

Upvotes: 2

Enlico
Enlico

Reputation: 28416

As you can see here, a range is just something on which you can call begin and end. Nothing more than that.

For instance, you can use the result of begin(range), which is an iterator, to traverse the range, using the ++ operator to advance it.

In general, looking back at the concept I linked above, you can use a range whenever the conext code only requires to be able to call begin and end on it.

Whether this is advisable or enough depends on what you need to do with it. Clearly, if your intention is to pass evens to a function which expects a std::vector (for instance it's a function you cannot change, and it calls .push_back on the entity we are talking about), you clearly have to make a std::vector out of filter's output, which I'd do via

auto evens = vec | ranges::views::filter(whatever) | ranges::to_vector;

but if all the function which you pass evens to does is to loop on it, then

return vec | ranges::views::filter(whatever);

is just fine.

As regards life time considerations, a view is to a range of values what a pointer is to the pointed-to entity: if the latter is destroied, the former will be dangling, and making improper use of it will be undefined behavior. This is an erroneous program:

#include <iostream>
#include <range/v3/view/filter.hpp>
#include <string>

using namespace ranges;
using namespace ranges::views;

auto f() {
    // a local vector here
    std::vector<std::string> vec{"zero","one","two","three","four","five"};
    // return a view on the local vecotor
    return vec | filter([](auto){ return true; });
} // vec is gone ---> the view returned is dangling

int main()
{
    // the following throws std::bad_alloc for me
    for (auto i : f()) {
        std::cout << i << std::endl;
    }
}

Upvotes: 3

Ranoiaetep
Ranoiaetep

Reputation: 6637

This was asked in op's comment section, but I think I will respond it in the answer section:

The Ranges library seems promising, but I'm a little apprehensive about this returning auto.

Remember that even with the addition of auto, C++ is a strongly typed language. In your case, since you are returning evens, then the return type will be the same type of evens. (technically it will be the value type of evens, but evens was a value type anyways)

In fact, you probably really don't want to type out the return type manually: std::ranges::filter_view<std::ranges::ref_view<const std::vector<int>>, MyClass::getEvens() const::<decltype([](int i) {return ! (i % 2);})>> (141 characters)


  • As mentioned by @Caleth in the comment, in fact, this wouldn't work either as evens was a lambda defined inside the function, and the type of two different lambdas will be different even if they were basically the same, so there's literally no way of getting the full return type here.

While there might be debates on whether to use auto or not in different cases, but I believe most people would just use auto here. Plus your evens was declared with auto too, typing the type out would just make it less readable here.


So what are my options if I want to access a subset (for instance even numbers)? Are there any other approaches I should be considering, with or without the Ranges library?

Depends on how you would access the returned data and the type of the data, you might consider returning std::vector<T*>.

views are really supposed to be viewed from start to end. While you could use views::drop and views::take to limit to a single element, it doesn't provide a subscript operator (yet).

There will also be computational differences. vector need to be computed beforehand, where views are computed while iterating. So when you do:

for(auto i : myObject.getEven())
{
    std::cout << i;
}

Under the hood, it is basically doing:

for(auto i : myObject.vec)
{
    if(!(i % 2)) std::cout << i;
}

Depends on the amount of data, and the complexity of computations, views might be a lot faster, or about the same as the vector method. Plus you can easily apply multiple filters on the same range without iterating through the data multiple times.

In the end, you can always store the view in a vector:

std::vector<int> vec2(evens.begin(), evens.end());

So my suggestions is, if you have the ranges library, then you should use it.

If not, then vector<T>, vector<T*>, vector<index> depending on the size and copiability of T.

Upvotes: 4

j__
j__

Reputation: 740

There's no restrictions on the usage of components of the STL in the standard. Of course, there are best practices (eg, string_view instead of string const &).

In this case, I can foresee no problems with handling the view return type directly. That said, the best practices are yet to be decided on since the standard is so new and no compiler has a complete implementation yet.

You're fine to go with the following, in my opinion:

class MyClass
{
public:
    MyClass(std::vector<int> v) : vec(std::move(v)) {}

    auto getEvens() const
    {
        return vec | ranges::views::filter([](int i) { return ! (i % 2); });
    }

private:
    std::vector<int> vec;
};

Upvotes: 2

Related Questions