Reputation: 423
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
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
Reputation: 3425
It is desirable to declare a function returning a range in a header and define it in a cpp file
However, there are complications that make it not advisable: How to get type of a view?
auto
ranges::any_view
Upvotes: 0
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
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
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)
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
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