Pre-C 20, it is necessary to use forwarding references in template functions when a std::ranges::range
is expected as a parameter. Since concepts are available in C 20, it is now possible to pass a std::ranges::view
by value to a generic function. Per the standard, a view is a range.
Consider the following code.
#include <vector>
#include <ranges>
#include <iterator>
#include <iostream>
template <std::ranges::range Range>
void fn1(Range range) // intentionally not a forwarding reference
{
for (auto& elem : range) {
elem;
}
}
template <std::ranges::view View>
void fn2(View view)
{
for (auto& elem : view) {
elem;
}
}
int main()
{
std::vector<int> v{1,2,3};
fn1(v); // doesn't increment, since a copy of 'v' is used in 'fn1'.
/* fn2(v); // fails to compile, since 'v' cannot be implicitly converted to a view */
fn1(std::views::all(v)); // increments, since a ref_view is passed to fn1
fn2(std::views::all(v)); // increments, as expected
for (int val : v)
std::cout << val << ' '; // 3 4 5
return 0;
}
I have nothing specifically against forwarding references. One can make the claim of a readability benefit to being able to directly pass an object that models a range into a generic function (e.g. fn1(v)
).
Is this purely a matter of preference, or are there other considerations when making the decision to implement the generic function parameter as Range&&
or View
?
CodePudding user response:
This is, broadly speaking, a bad idea.
Most ranges are not views. The ranges::view
concept requires that a type opt-into being a view. Containers are not views; they're containers. span
is a view, but vector
is not. Also, most containers cannot be views, as view requires that copying/moving is a constant-time operation. And for most containers, this is not possible.
So by constraining your template to view
specifically, you basically force the user to wrap anything they pass to the function in some kind of view type. This creates a lot of syntactic noise on the caller side that serves no useful purpose.
CodePudding user response:
There are basically several ways that you could conceive of declaring an algorithm that takes some kind of range:
- constrain on
range
vs constrain onview
- take by
T&&
,T const&
,T&
, orT
That cartesian product gives you 8 options to consider.
The problem with T const&
, even for non-mutating algorithms, is that we have non-const-iterable ranges (range<R>
doesn't necessitate range<R const>
). There may a hypothetical alternative design where view
actually requires const-iteration, but that's not the design we have. So while T const&
seems like the obviously right choice for non-mutating algorithms, it ends up limiting functionality a lot.
The problem with T&
is that Ranges leads to many situations of having rvalue ranges, and taking a T&
would prevent users from writing algo(r | views::transform(f))
-- and it's basically the selling point of the library to be able to write that.
That leaves T
and T&&
.
By-value range -- template <ranges::range R> void f(R r)
-- is problematic since ranges can be arbitrarily expensive to copy (bad for performance) and can be owning (in which case your algorithm probably isn't doing what you think it's doing -- it may do the right thing for some types but not others).
Forwarding-reference view -- template <ranges::view V> void g(V&& v)
-- actually disallows passing lvalues, because V&
is never a view
, so likewise is surprisingly limited in functionality.
This effectively reduces the universe to two options:
template <ranges::range R> void f(R&& r);
template <ranges::view V> void g(V v);
These do have functional differences. If I have a non-copyable view, then f(move_only_view)
would work while g(move_only_view)
would not. But then the reason you'd have a move-only view is that it's an input view, so perhaps g(std::move(move_only_view))
would be syntactically better anyway. Otherwise, f(copyable_view)
doesn't copy the view while g(copyable_view)
does. Copying views is cheap, as in doesn't copy all the elements, but it's not necessarily free (consider a transform_view
with an arbitrarily fat projection, or just many layers of adaptors). So the forwarding-reference-range version does offer the most functionality, and is the cheapest.
Other than that, these two are quite similar. f(e)
and g(views::all(e))
, when they both compile, are basically the same? Which kind of begs the question of why you'd write the value-view
version over the forwarding-range
one. There's not really a significant motivator for doing so.
The primary place where you need view
, specifically, rather than range
is when constructing range adaptors. Those need to have member view
s (not range
s), since they themselves need to be view
s. But even there, the entry-point isn't:
template <ranges::view V> auto make_adaptor(V );
It's, instead:
template <ranges::viewable_range R> auto make_adaptor(R&& );
That is, we still take a forwarding range
- it's a refined version of range
that ensures that we can actually convert it to a view
.