3797. elements_view insufficiently constrained

Section: 25.7.23.2 [range.elements.view] Status: New Submitter: Hui Xie Opened: 2022-10-21 Last modified: 2022-11-01

Priority: 2

View all other issues in [range.elements.view].

View all issues with New status.

Discussion:

This issue came up when I tried to integrate the C++23 changes to tuple-like into ranges::elements_view in libc++. Given the following test:

Using SubRange = ranges::subrange<MoveOnlyIter, Sent>;
std::vector<SubRange> srs = ...;  // a vector of subranges
for(auto&& iter : srs | views::elements<0>){
}

The above code results in a hard error in deciding the iterator_category (The base is a random access range so it should exist). The immediate hard error complains that the following expression is invalid.

std::get<N>(*current_);

Note that even if iterator_category does not complain, it will complain later when we dereference the iterator.

Here are the declarations of the "get" overloads for subrange:

template<size_t N, class I, class S, subrange_kind K>
  requires ((N == 0 && copyable<I>) || N == 1)
  constexpr auto get(const subrange<I, S, K>& r);

template<size_t N, class I, class S, subrange_kind K>
  requires (N < 2)
  constexpr auto get(subrange<I, S, K>&& r);

Note that the first overload requires copyable<I> which is false and the second overload requires an rvalue, which is also not the case. So we don't have a valid "get" in this case.

But why does elements_view allow the instantiation in the first place? Let's look at its requirements:

template<class T, size_t N>
  concept returnable-element =                  // exposition only
    is_reference_v<T> || move_constructible<tuple_element_t<N, T>>;

template<input_range V, size_t N>
    requires view<V> && has-tuple-element<range_value_t<V>, N> &&
             has-tuple-element<remove_reference_t<range_reference_t<V>>, N> &&
             returnable-element<range_reference_t<V>, N>
  class elements_view;

It passed the "is_reference_v<range_reference_t<V>>" requirement, because it is "subrange&". Here the logic has an assumption: if the tuple-like is a reference, then we can always "get" and return a reference. This is not the case for subrange. subrange's get always return by value.

[2022-11-01; Reflector poll]

Set priority to 2 after reflector poll.

"The actual issue is that P2165 broke has-tuple-element for this case. We should unbreak it."

Proposed resolution:

This wording is relative to N4917.

[Drafting Note: Three mutually exclusive options are prepared, depicted below by Option A, Option B, and Option C, respectively.]

Option A: Properly disallow this case (preferred solution)

  1. Modify 25.7.23.2 [range.elements.view] as indicated:

    namespace std::ranges {
      […]
      template<class T, size_t N>
      concept returnable-element =              // exposition only
        requires { std::get<N>(declval<T>()); } &&
        is_reference_v<T> || move_constructible<tuple_element_t<N, T>>;  
      […]
    }
    

Option B: Relax subrange's get to have more overloads. Since subrange's non-const begin unconditionally moves the iterator (even for lvalue-reference),

[[nodiscard]] constexpr I begin() requires (!copyable<I>);
Effects: Equivalent to: return std::move(begin_);

if we add more get overloads, it would work. The non-const lvalue-ref overload would work (and it also moves because non-const lvalue begin moves). This solution would make another way to let subrange's iterator in moved-from state, which is not good.

  1. Modify 25.2 [ranges.syn] as indicated:

    […]
    namespace std::ranges {
      […]
    
      template<size_t N, class I, class S, subrange_kind K>
        requires ((N == 0 && copyable<I>) || N == 1)
        constexpr auto get(const subrange<I, S, K>& r);
    
      template<size_t N, class I, class S, subrange_kind K>
        requires (N < 2)
        constexpr auto get(subrange<I, S, K>&& r);
        
      template<size_t N, class I, class S, subrange_kind K>
        requires ((N == 0 && constructible_from<I, const I&&>) || N == 1)
        constexpr auto get(const subrange<I, S, K>&& r);
      
      template<size_t N, class I, class S, subrange_kind K>
        requires (N < 2)
        constexpr auto get(subrange<I, S, K>& r);
    }
    […]
    

Option C: Make subrange's get to return by reference. This seems to significantly change the subrange's tuple protocol, which is not ideal.