I'm having trouble understanding some ideas and design decisions in the v3 ranges library. A lot of the view adaptors (e.g. ranges::views::transform
) make a copy of a view (or create a ref view to a container) they receive as input (by using ranges::views::all
), but other adaptors do not, such as ranges::views::slice
. This seems like an inconsistency which can cause unexpected behavior if one is not really careful. Consider the following code where one makes a view in a local scope and returns an adapted view:
#include <iostream>
#include <vector>
#include <range/v3/all.hpp>
auto get_slice(const std::vector<int>& vec) {
auto symmetric = ranges::views::concat(
vec | ranges::views::reverse,
ranges::views::single(-1),
vec
);
for (auto x : symmetric)
std::cout << x << " ";
std::cout << std::endl;
using view_type = decltype(symmetric);
static_assert(!ranges::borrowed_range<view_type>);
static_assert(ranges::borrowed_range<view_type&>);
// unexpected UB
return symmetric | ranges::views::slice(size_t(1), size_t(2 * vec.size()));
}
int main() {
std::vector<int> vec { 0, 1, 2, 3, 4, 5 };
for (int x : get_slice(vec))
std::cout << x << std::endl;
return 0;
}
In the return line of get_slice, symmetric
is treated as a borrowed range since it is an lvalue, thus one gets undefined behavior when it goes out of scope since ranges::views::slice
uses an lvalue's iterators directly. In this case in the return line one should either make a copy by using ranges::views::all
or use std::move since then slice would make a copy of the view for itself. If one wanted to use ranges::views::transform
instead of slice, then one would have not needed to make a copy or do a move of the view - transform does this for you.
Why do some adaptors always make a copy of even a borrowed view, and others do not but rather use iterators of a borrowed view directly, as does slice? Theoretically, one could always use the iterators directly in the case of borrowed range, no? Does this mean that for consistency reasons one should always make a copy of a view manually as was necessary above, even if this is not actually needed as would be the case when using ranges::views::transform
?
It also seems to me that this problem would disappear if one did not treat lvalue references of non-borrowed views as borrowed ranges, though this would / could make the concept less consistent.
TLDR:
- Why do some ranges adaptors make copies of views and others do not?
- Why does slice not make a copy of an lvalue view, but rather directly uses its iterators to make a subrange? Is this actually intended and / or useful?
Is there a benefit to treating an lvalue reference to a view which is not a borrowed range as a borrowed range?(one can use std algorithms which return an iterator of a view)