C++23 introduced the very powerful ranges::to
for constructing an object (usually a container) from a range, with the following definition ([range.utility.conv.to]):
template<class C, input_range R, class... Args> requires (!view<C>)
constexpr C to(R&& r, Args&&... args);
Note that it only constrains the template parameter C
not to be a view
, that is, C
may not even be a range
.
However, its implementation uses range_value_t<C>
to obtain the element type of C
, which makes C
at least a range
given the range_value_t
constraint that the template parameter R
must model a range
.
So, why is ranges::to
so loosely constrained on the template parameter C
?
I noticed that the R3 version of the paper used to constrain C
to be input_range
, which was apparently reasonable since input_range
guaranteed that the range_value_t
to be well-formed, but in R4 this constraint was removed. And I didn't find any comments about this change.
So, what are the considerations for removing the constraint that C
must be input_range
?
Is there a practical example of the benefits of this constraint relaxation?
This is a problem with the wording that we'll need to address, I'll open an issue later today. This is LWG 3785.
So, what are the considerations for removing the constraint that
C
must beinput_range
?
The goal of ranges::to
is to collect a range into... something. But it need not be an actual range. Just something which consumes all the elements. Of course, the most common usage will be an actual container type, and the most common actual container type will be std::vector
.
There are other interesting use-cases though, that there really isn't much reason to reject.
Let's say we have a range of std::expected<int, std::exception_ptr>
, call it results
. Maybe we ran a bunch of computations and maybe some of them failed. I could collect that into a std::vector<std::expected<int, std::exception_ptr>>
, and that might be useful. But there's another alternative: I could collect it into a std::expected<std::vector<int>, std::exception_ptr>
. That is, if all of the computations succeeded, I get as a value type all of the results. However, if any of them failed, I get the first error. That's a very useful thing to be able to do, that is very much conceptually in line with what ranges::to
is doing to its input - so this could support:
auto processed = results | ranges::to<std::expected>();
if (not processed) {
std::rethrow_exception(processed.error());
}
std::vector<int> values = std::move(processed).value();
// go do more stuff
This is quite useful to support - especially since it doesn't really cost anything to not support it. We just have to not prematurely reject it.