Suppose I have a generate_my_range
class that models a range
(in particular, is regular
). Then is the following code correct:
auto generate_my_range(int some_param) {
auto my_transform_op = [](const auto& x){ return do_sth(x); };
return my_custom_rng_gen(some_param) | ranges::views::transform(my_transform_op);
}
auto cells = generate_my_range(10) | ranges::to<std::vector>;
Is my_custom_rng_gen(some_param)
taken by value by the (first) pipe operator, or do I have a dangling reference once I leave the generate_my_range
scope ?
Would it be the same with the functionnal call ranges::views::transform(my_custom_rng_gen(some_param),my_transform_op)
?
Would it be correct if I used a lvalue reference? e.g.:
auto generate_my_range(int some_param) {
auto my_transform_op = [](const auto& x){ return do_sth(x); };
auto tmp_ref = my_custom_rng_gen(some_param);
return tmp_ref | ranges::views::transform(my_transform_op);
}
If ranges are taken by values for these operations, then what do I do if I pass an lvalue ref to a container ? Should I use a ranges::views::all(my_container)
pattern ?
In ranges library there are two kind of operations:
Views are lightweight. You pass them by value and require the underlying containers to remain valid and unchanged.
From the ranges-v3 documentation
A view is a lightweight wrapper that presents a view of an underlying sequence of elements in some custom way without mutating or copying it. Views are cheap to create and copy and have non-owning reference semantics.
and:
Any operation on the underlying range that invalidates its iterators or sentinels will also invalidate any view that refers to any part of that range.
The destruction of the underlying container obviously invalidates all iterators to it.
In your code you are specifially using views -- You use ranges::views::transform
. The pipe is merely a syntactic sugar to makes it easy to write the way it is. You should look at the last thing in the pipe to see what you produce - in your case, it is a view.
If there was no pipe operator, it would probably look something like this:
ranges::views::transform(my_custom_rng_gen(some_param), my_transform_op)
if there were multiple transformations connected that way you can see how ugly it would get.
Thus, if my_custom_rng_gen
produces some kind of a container, that you transform and then return, that container gets destroyed and you have dangling references from your view. If my_custom_rng_gen
is another view to a container that lives outside these scopes, everything is fine.
However, the compiler should be able to recognize that you are applying a view on a temporary container and hit you with a compile error.
If you want your function to return a range as a container, you need to explicitly "materialize" the result. For that, use the ranges::to
operator within the function.
Update: To be more rexplicit regarding your comment "where does the documentation says that composing range / piping takes and stores a view?"
Pipe is merely a syntactic sugar to connect things in an easy-to-read expression. Depending on how it is used, it may or may not return a view. It depends on the right-hand side argument. In your case it is:
`<some range> | ranges::views::transform(...)`
So the expression returns whatever views::transform
returns.
Now, by reading the documentation of the transform:
Below is a list of the lazy range combinators, or views, that Range-v3 provides, and a blurb about how each is intended to be used.
[...]
views::transform
Given a source range and a unary function, return a new range where each result element is the result of applying the unary function to a source element.
So it returns a range, but since it is a lazy operator, that range it returns is a view, with all its semantics.
Taken from the ranges-v3 documentation:
Views [...] have non-owning reference semantics.
and
Having a single range object permits pipelines of operations. In a pipeline, a range is lazily adapted or eagerly mutated in some way, with the result immediately available for further adaptation or mutation. Lazy adaption is handled by views, and eager mutation is handled by actions.
// taken directly from the the ranges documentation
std::vector<int> const vi{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
using namespace ranges;
auto rng = vi | views::remove_if([](int i){ return i % 2 == 1; })
| views::transform([](int i){ return std::to_string(i); });
// rng == {"2","4","6","8","10"};
In the code above, rng simply stores a reference to the underlying data and the filter and transformation functions. No work is done until rng is iterated.
Since you said that the temporary range can be thought of as a container, your function returns a dangling reference.
In other words, you need to make sure that the underlying range outlives the view, or you are in trouble.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With