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?

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • I would think `requires(input_range)` = "shall not participate in overload resolution if not input_range, SFINAE/concepts friendly", but [being ill-formed if not range](https://eel.is/c++draft/range.utility.conv.to#1.3) allows a `static_assert`, like in [this implementation](https://github.com/microsoft/STL/blob/82c81e8fb12fa457344c7c7f8a2f0a7e38263a77/stl/inc/ranges#L6924). Unsure how this interacts with your `range_­value_­t` point. – Artyer Sep 19 '22 at 04:53

1 Answers1

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 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? – 康桓瑋 Sep 22 '22 at 08:09
  • 1
    @康桓瑋 That doesn't really make any sense? You wouldn't `to>`, you'd `to>`. `to` isn't really fallible, so what would `to>` do if the range wasn't exactly one element? And then the rest are even less meaningful. – Barry Sep 22 '22 at 16:39