Why does C++23 ranges::to not constrain the container type C to be a range?
Asked Answered
A

1

5

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?

Actuary answered 19/9, 2022 at 4:6 Comment(1)
I would think requires(input_range<C>) = "shall not participate in overload resolution if not input_range, SFINAE/concepts friendly", but being ill-formed if not range allows a static_assert, like in this implementation. Unsure how this interacts with your range_­value_­t<C> point.En
R
4

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 be input_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.

Reganregard answered 19/9, 2022 at 16:12 Comment(3)
Thanks for the comprehensive answer, which solved my confusion. But what I don't quite understand is why C can't be a view (such as owning_view or single_view which actually owns elements). What potential problems does it bring?Yeomanry
@Actuary That doesn't really make any sense? You wouldn't to<owning_view<vector<T>>, you'd to<vector<T>>. to isn't really fallible, so what would to<single_view<T>> do if the range wasn't exactly one element? And then the rest are even less meaningful.Reganregard
I thought it might still be useful to remove the constraint that C is not a view, I often need to convert subranges to a string_views, and this relaxation allows me to spell rng | views::transform(ranges::to<string_view>()).Yeomanry

© 2022 - 2024 — McMap. All rights reserved.