3731. zip_view and adjacent_view are underconstrained

Section: 25.7.25.2 [range.zip.view], 25.7.27.2 [range.adjacent.view], 25.7.33.2 [range.cartesian.view] Status: New Submitter: Hewill Kang Opened: 2022-07-04 Last modified: 2023-08-12

Priority: 3

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

View all issues with New status.

Discussion:

Both zip_view::iterator's (25.7.25.3 [range.zip.iterator]) and adjacent_view::iterator's (25.7.27.3 [range.adjacent.iterator]) operator* have similar Effects: elements:

return tuple-transform([](auto& i) -> decltype(auto) { return *i; }, current_);

where tuple-transform is defined as:

template<class F, class Tuple>
constexpr auto tuple-transform(F&& f, Tuple&& tuple) { // exposition only
  return apply([&]<class... Ts>(Ts&&... elements) {
    return tuple-or-pair<invoke_result_t<F&, Ts>...>(
      invoke(f, std::forward<Ts>(elements))...
    );
  }, std::forward<Tuple>(tuple));
}

That is, zip_view::iterator will invoke the operator* of each iterator of Views and return a tuple containing its reference.

This is not a problem when the reference of iterators is actually the reference type. However, when the operator* returns a prvalue of non-movable type, tuple-transform will be ill-formed since there are no suitable constructors for tuple:

#include <ranges>

struct NonMovable {
  NonMovable() = default;
  NonMovable(NonMovable&&) = delete;
};

auto r = std::views::iota(0, 5)
       | std::views::transform([](int) { return NonMovable{}; });
auto z = std::views::zip(r);
auto f = *z.begin(); // hard error

We should constrain the range_reference_t of the underlying range to be move_constructible when it is not a reference type, which also solves similar issues in zip_view::iterator and adjacent_view::iterator's operator[] and iter_move.

[2022-08-23; Reflector poll]

Set priority to 3 after reflector poll. "The constraint should just be move_constructible."

Previous resolution [SUPERSEDED]:

This wording is relative to N4910.

  1. Modify 25.2 [ranges.syn], header <ranges> synopsis, as indicated:

    namespace std::ranges {
      […]
      // 25.7.25 [range.zip], zip view
      template<class Ref>
        concept tuple-constructible-reference = see below; // exposition only
    
      template<input_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0) &&
                 (tuple-constructible-reference<range_reference_t<Views>> && ...)
      class zip_view;
    
      […]
    
      // 25.7.27 [range.adjacent], adjacent view
      template<forward_range V, size_t N>
        requires view<V> && (N > 0) && 
                 tuple-constructible-reference<range_reference_t<V>>
      class adjacent_view;
    }
    […]
    
  2. Modify 25.7.25.2 [range.zip.view] as indicated:

    namespace std::ranges {
      template<class Ref>
        concept tuple-constructible-reference =            // exposition only
          is_reference_v<Ref> || move_constructible<Ref>;
      […]
      template<input_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0) &&
                 (tuple-constructible-reference<range_reference_t<Views>> && ...)
      class zip_view : public view_interface<zip_view<Views...>> {
        […]
      };
      […]
    }
    
  3. Modify 25.7.27.2 [range.adjacent.view] as indicated:

    namespace std::ranges {
      template<forward_range V, size_t N>
        requires view<V> && (N > 0) && 
                 tuple-constructible-reference<range_reference_t<V>>
      class adjacent_view : public view_interface<adjacent_view<V, N>> {
        […]
      };
      […]
    }
    

[2022-09-25; Hewill provides improved wording]

Previous resolution [SUPERSEDED]:

This wording is relative to N4917.

  1. Modify 25.2 [ranges.syn], header <ranges> synopsis, as indicated:

    namespace std::ranges {
      […]
    
      template<class R>
        concept has-tuplable-ref =            // exposition only
          move_constructible<range_reference_t<R>>;
    
      // 25.7.25 [range.zip], zip view
      template<input_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0) &&
                 (has-tuplable-ref<Views> && ...)
      class zip_view;
    
      […]
    
      // 25.7.27 [range.adjacent], adjacent view
      template<forward_range V, size_t N>
        requires view<V> && (N > 0) && has-tuplable-ref<V>
      class adjacent_view;
    
      […]
    
      // 25.7.33 [range.cartesian], cartesian product view
      template<input_range First, forward_range... Vs>
        requires (view<First> && ... && view<Vs>) &&
                 (has-tuplable-ref<First> && ... && has-tuplable-ref<Vs>)
      class cartesian_product_view;
    
      […]
    }
    
  2. Modify 25.7.25.2 [range.zip.view] as indicated:

    namespace std::ranges {
      […]
      
      template<input_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0) &&
                 (has-tuplable-ref<Views> && ...)
      class zip_view : public view_interface<zip_view<Views...>> {
        […]
      };
    }
    
  3. Modify 25.7.25.3 [range.zip.iterator] as indicated:

    namespace std::ranges {
      […]
      template<input_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0) &&
                 (has-tuplable-ref<Views> && ...)
      template<bool Const>
      class zip_view<Views...>::iterator {
        […]
      };
    }
    
  4. Modify 25.7.25.4 [range.zip.sentinel] as indicated:

    namespace std::ranges {
      template<input_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0) &&
                 (has-tuplable-ref<Views> && ...)
      template<bool Const>
      class zip_view<Views...>::sentinel {
        […]
      };
    }
    
  5. Modify 25.7.27.2 [range.adjacent.view] as indicated:

    namespace std::ranges {
      template<forward_range V, size_t N>
        requires view<V> && (N > 0) && has-tuplable-ref<V>
      class adjacent_view : public view_interface<adjacent_view<V, N>> {
        […]
      };
    }
    
  6. Modify 25.7.27.3 [range.adjacent.iterator] as indicated:

    namespace std::ranges {
      template<forward_range V, size_t N>
        requires view<V> && (N > 0) && has-tuplable-ref<V>
      template<bool Const>
      class adjacent_view<V, N>::iterator {
        […]
      };
    }
    
  7. Modify 25.7.27.4 [range.adjacent.sentinel] as indicated:

    namespace std::ranges {
      template<forward_range V, size_t N>
        requires view<V> && (N > 0) && has-tuplable-ref<V>
      template<bool Const>
      class adjacent_view<V, N>::sentinel {
        […]
      };
    }
    
  8. Modify 25.7.33.2 [range.cartesian.view] as indicated:

    namespace std::ranges {
      […]
      template<input_range First, forward_range... Vs>
        requires (view<First> && ... && view<Vs>) &&
                 (has-tuplable-ref<First> && ... && has-tuplable-ref<Vs>)
      class cartesian_product_view : public view_interface<cartesian_product_view<First, Vs...>> {
        […]
      };
    }
    
  9. Modify 25.7.33.3 [range.cartesian.iterator] as indicated:

    namespace std::ranges {
      template<input_range First, forward_range... Vs>
        requires (view<First> && ... && view<Vs>) &&
                 (has-tuplable-ref<First> && ... && has-tuplable-ref<Vs>)
      template<bool Const>
      class cartesian_product_view<First, Vs...>::iterator {
        […]
      };
    }
    

[2023-08-08; Hewill provides improved wording]

Proposed resolution:

This wording is relative to N4950.

  1. Modify 26.2 25.2 [ranges.syn], header <ranges> synopsis, as indicated:

    namespace std::ranges {
      […]
    
      // 25.5.2 [range.utility.helpers], helper concepts
      template<class R>
        concept range-with-movable-references = see below; // exposition only
    
      // 25.5.3 [view.interface], class template view_interface
      template<class D>
        requires is_class_v<D> && same_as<D, remove_cv_t<D>>
      class view_interface;                                                             // freestanding
    
      […]
    
      // 25.7.24 [range.enumerate], enumerate view
      template<input_rangeview V>
        requires range-with-movable-references<V>view<View>
      class enumerate_view;                                                             // freestanding
    
      […]
    
      // 25.7.25 [range.zip], zip view
      template<range-with-movable-referencesinput_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0)
      class zip_view;                                                                   // freestanding
    
      […]
    
      // 25.7.27 [range.adjacent], adjacent view
      template<range-with-movable-referencesforward_range V, size_t N>
        requires forward_range<V> && view<V> && (N > 0)
      class adjacent_view;                                                              // freestanding
    
      […]
    
      // 25.7.33 [range.cartesian], cartesian product view
      template<range-with-movable-referencesinput_range First, 
               range-with-movable-referencesforward_range... Vs>
        requires (view<First> && ... && (forward_range<Vs> && view<Vs>))
      class cartesian_product_view;                                                     // freestanding
    
      […]
    }
    
  2. Modify 25.7.25.2 [range.zip.view] as indicated:

    namespace std::ranges {
      […]
      
      template<range-with-movable-referencesinput_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0)
      class zip_view : public view_interface<zip_view<Views...>> {
        […]
      public:
        […]
        constexpr auto begin() const requires (range-with-movable-referencesrange<const Views> && ...) {
          return iterator<true>(tuple-transform(ranges::begin, views_));
        }
        […]
        constexpr auto end() const requires (range-with-movable-referencesrange<const Views> && ...) {
          if constexpr (!zip-is-common<const Views...>) {
            return sentinel<true>(tuple-transform(ranges::end, views_));
          } else if constexpr ((random_access_range<const Views> && ...)) {
            return begin() + iter_difference_t<iterator<true>>(size());
          } else {
            return iterator<true>(tuple-transform(ranges::end, views_));
          }
        }
        […]
      };
    }
    
  3. Modify 25.7.25.3 [range.zip.iterator] as indicated:

    namespace std::ranges {
      […]
      template<range-with-movable-referencesinput_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0)
      template<bool Const>
      class zip_view<Views...>::iterator {
        […]
      };
    }
    
  4. Modify 25.7.25.4 [range.zip.sentinel] as indicated:

    namespace std::ranges {
      template<range-with-movable-referencesinput_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0)
      template<bool Const>
      class zip_view<Views...>::sentinel {
        […]
      };
    }
    
  5. Modify 25.7.27.2 [range.adjacent.view] as indicated:

    namespace std::ranges {
      template<range-with-movable-referencesforward_range V, size_t N>
        requires forward_range<V> && view<V> && (N > 0)
      class adjacent_view : public view_interface<adjacent_view<V, N>> {
        […]
      public:
        […]
        constexpr auto begin() const requires range-with-movable-referencesrange<const V> {
          return iterator<true>(ranges::begin(base_), ranges::end(base_));
        }
        […]
        constexpr auto end() const requires range-with-movable-referencesrange<const V> {
          if constexpr (common_range<const V>) {
            return iterator<true>(as-sentinel{}, ranges::begin(base_), ranges::end(base_));
          } else {
            return sentinel<true>(ranges::end(base_));
          }
        }
        […]
      };
    }
    
  6. Modify 25.7.27.3 [range.adjacent.iterator] as indicated:

    namespace std::ranges {
      template<range-with-movable-referencesforward_range V, size_t N>
        requires forward_range<V> && view<V> && (N > 0)
      template<bool Const>
      class adjacent_view<V, N>::iterator {
        […]
      };
    }
    
  7. Modify 25.7.27.4 [range.adjacent.sentinel] as indicated:

    namespace std::ranges {
      template<range-with-movable-referencesforward_range V, size_t N>
        requires forward_range<V> && view<V> && (N > 0)
      template<bool Const>
      class adjacent_view<V, N>::sentinel {
        […]
      };
    }
    
  8. Modify 25.7.33.2 [range.cartesian.view] as indicated:

    namespace std::ranges {
      […]
      template<range-with-movable-referencesinput_range First, 
               range-with-movable-referencesforward_range... Vs>
        requires (view<First> && ... && (forward_range<Vs> && view<Vs>))
      class cartesian_product_view : public view_interface<cartesian_product_view<First, Vs...>> {
        […]
      public:
        […]
        constexpr iterator<true> begin() const
          requires (range-with-movable-referencesrange<const First> && ... && 
                    range-with-movable-referencesrange<const Vs>);
        […]
        constexpr iterator<true> end() const
          requires (range-with-movable-references<const First> && ... && 
                    range-with-movable-references<const Vs>) &&
                   cartesian-product-is-common<const First, const Vs...>;
        […]
      };
    }
    
    […]
    constexpr iterator<true> begin() const
      requires (range-with-movable-referencesrange<const First> && ... && 
                range-with-movable-referencesrange<const Vs>);
    

    -3- Effects: Equivalent to:

    return iterator<true>(*this, tuple-transform(ranges::begin, bases_));
    
    constexpr iterator<false> end()
      requires ((!simple-view<First> || ... || !simple-view<Vs>)
        && cartesian-product-is-common<First, Vs...>);
    constexpr iterator<true> end() const
      requires (range-with-movable-references<const First> && ... && range-with-movable-references<const Vs>)
        && cartesian-product-is-common<const First, const Vs...>;
    

    -4- Let:

    […]
  9. Modify 25.7.33.3 [range.cartesian.iterator] as indicated:

    namespace std::ranges {
      […]
      template<range-with-movable-referencesinput_range First, range-with-movable-referencesforward_range... Vs>
        requires (view<First> && ... && (forward_range<Vs> && view<Vs>))
      class cartesian_product_view<First, Vs...>::iterator {
        […]
      };
    }