elements_view::begin() and elements_view::end() have incompatible constraints
Patrick Palka

Created on 2020-02-21.00:00:00 last changed 1 month ago


Date: 2020-02-23.12:04:17

Proposed resolution:

This wording is relative to N4849 after application of P1994R1.

  1. Modify [range.elements.view], class template elements_view synopsis, as indicated:

    namespace std::ranges {
      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>
      class elements_view : public view_interface<elements_view<V, N>> {
        constexpr V base() && { return std::move(base_); }
        constexpr auto begin() requires (!simple-view<V>)
        { return iterator<false>(ranges::begin(base_)); }
        constexpr auto begin() const requires simple-view<V>range<const V>
        { return iterator<true>(ranges::begin(base_)); }    
Date: 2020-02-21.00:00:00

P1994R1 (elements_view needs its own sentinel) introduces a distinct sentinel type for elements_view. In doing so, it replaces the two existing overloads of elements_view::end() with four new ones:

-    constexpr auto end() requires (!simple-view<V>)
-    { return ranges::end(base_); }
-    constexpr auto end() const requires simple-view<V>
-    { return ranges::end(base_); }

+    constexpr auto end()
+    { return sentinel<false>{ranges::end(base_)}; }
+    constexpr auto end() requires common_range<V>
+    { return iterator<false>{ranges::end(base_)}; }
+    constexpr auto end() const
+      requires range<const V>
+    { return sentinel<true>{ranges::end(base_)}; }
+    constexpr auto end() const
+      requires common_range<const V>
+    { return iterator<true>{ranges::end(base_)}; }

But now these new overloads of elements_view::end() have constraints that are no longer consistent with the constraints of elements_view::begin():

     constexpr auto begin() requires (!simple-view<V>)
     { return iterator<false>(ranges::begin(base_)); }
     constexpr auto begin() const requires simple-view<V>
     { return iterator<true>(ranges::begin(base_)); }

This inconsistency means that we can easily come up with a view V for which elements_view<V>::begin() returns an iterator<true> and elements_view<V>::end() returns an sentinel<false>, i.e. incomparable things of opposite constness. For example:

tuple<int, int> x[] = {{0,0}};
ranges::subrange r = {counted_iterator(x, 1), default_sentinel};
auto v = r | views::elements<0>;
v.begin() == v.end(); // ill-formed

Here, overload resolution for begin() selects the const overload because the subrange r models simple-view. But overload resolution for end() selects the non-const non-common_range overload. Hence the last line of this snippet is ill-formed because it is comparing an iterator and sentinel of opposite constness, for which we have no matching operator== overload. So in this example v does not even model range because its begin() and end() are incomparable.

This issue can be resolved by making sure the constraints on elements_view::begin() and on elements_view::end() are consistent and compatible. The following proposed resolution seems to be one way to achieve that and takes inspiration from the design of transform_view.

Date User Action Args
2020-02-23 12:04:17adminsetmessages: + msg11145
2020-02-21 00:00:00admincreate