This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.

### 3797. `elements_view` insufficiently constrained

Section: 26.7.22.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 26.7.22.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 26.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.