3785. ranges::to is over-constrained on the destination type being a range

Section: 25.5.7.2 [range.utility.conv.to] Status: C++23 Submitter: Barry Revzin Opened: 2022-09-19 Last modified: 2024-01-29

Priority: Not Prioritized

View other active issues in [range.utility.conv.to].

View all other issues in [range.utility.conv.to].

View all issues with C++23 status.

Discussion:

The current wording in 25.5.7.2 [range.utility.conv.to] starts:

If convertible_to<range_reference_t<R>, range_value_t<C>> is true

and then tries to do C(r, args...) and then C(from_range, r, args...). The problem is that C might not be a range — indeed we explicitly removed that requirement from an earlier revision of the paper — which makes this check ill-formed. One example use-case is using ranges::to to convert a range of expected<T, E> into a expected<vector<T>, E>expected isn't any kind of range, but it could support this operation, which is quite useful. Unfortunately, the wording explicitly rejects that. This change happened between R6 and R7 of the paper and seems to have unintentionally rejected this use-case.

[2022-09-28; Reflector poll]

Set status to Tentatively Ready after five votes in favour during reflector poll. During telecon review we agreed that supporting non-ranges was an intended part of the original design, but was inadvertently broken when adding range_value_t for other reasons.

[2022-11-12 Approved at November 2022 meeting in Kona. Status changed: Voting → WP.]

Proposed resolution:

This wording is relative to N4917.

  1. Modify 25.5.7.2 [range.utility.conv.to] as indicated:

    [Drafting note: We need to be careful that this short-circuits, since if C does not satisfy input_range, then range_value_t<C> will be ill-formed.]

    template<class C, input_range R, class... Args> requires (!view<C>)
      constexpr C to(R&& r, Args&&... args);
    

    -1- Returns: An object of type C constructed from the elements of r in the following manner:

    1. (1.1) — If C does not satisfy input_range or convertible_to<range_reference_t<R>, range_value_t<C>> is true:

      1. (1.1.1) — If constructible_from<C, R, Args...> is true:

        C(std::forward<R>(r), std::forward<Args>(args)...)
      2. (1.1.2) — Otherwise, if constructible_from<C, from_range_t, R, Args...> is true:

        C(from_range, std::forward<R>(r), std::forward<Args>(args)...)
      3. (1.1.3) — Otherwise, if

        1. (1.1.3.1) — common_range<R> is true,

        2. (1.1.3.2) — cpp17-input-iterator<iterator_t<R>> is true, and

        3. (1.1.3.3) — constructible_from<C, iterator_t<R>, sentinel_t<R>, Args...> is true:

          C(ranges::begin(r), ranges::end(r), std::forward<Args>(args)...)
      4. (1.1.4) — Otherwise, if

        1. (1.1.4.1) — constructible_from<C, Args...> is true, and

        2. (1.1.4.2) — container-insertable<C, range_reference_t<R>> is true:

          C c(std::forward<Args>(args)...);
          if constexpr (sized_range<R> && reservable-container<C>)
            c.reserve(ranges::size(r));
          ranges::copy(r, container-inserter<range_reference_t<R>>(c));
          
    2. (1.2) — Otherwise, if input_range<range_reference_t<R>> is true:

      to<C>(r | views::transform([](auto&& elem) {
        return to<range_value_t<C>>(std::forward<decltype(elem)>(elem));
      }), std::forward<Args>(args)...);
      
    3. (1.3) — Otherwise, the program is ill-formed.