4089. concat_view::iterator's iter_swap is overconstrained

Section: 25.7.18.3 [range.concat.iterator] Status: New Submitter: Hewill Kang Opened: 2024-04-30 Last modified: 2024-06-24

Priority: 3

View other active issues in [range.concat.iterator].

View all other issues in [range.concat.iterator].

View all issues with New status.

Discussion:

When the underlying iterator of two concat_view::iterators are of different types, their iter_swap will dispatch ranges::swap to swap elements, which is reflected in its constraints of swappable_with<iter_reference_t<iterator>, iter_reference_t<iterator>>.

However, when the underlying iterators are all of the same type, ranges::swap will never be invoked, making checking for swappable_with unnecessary in this case.

This results in the current wording rejecting the following:

struct I {
  using value_type = int;
  using difference_type = int;
  std::reference_wrapper<int> operator*() const;
  I& operator++();
  void operator++(int);
  bool operator==(const I&) const;
  friend void iter_swap(const I&, const I&); // custom iter_swap
};

static_assert(std::indirectly_swappable<I>);

int main() {
  std::ranges::subrange<I, I> s1, s2;
  std::ranges::swap_ranges(s1, s2);
  std::ranges::concat_view c1{s1}, c2{s2};
  std::ranges::swap_ranges(c1, c2); // ill-formed
}

Because reference_wrapper does not satisfy swappable_with, concat_view::iterator does not have a valid iter_swap, leading to the constraints of swap_ranges being unsatisfied.

This doesn't seem like it should be, and I think providing a simplified homogeneous iter_swap specialization for concat_view::iterator is reasonable.

[2024-06-24; Reflector poll]

Set priority to 3 after reflector poll. An extreme corner case, probably not worth caring about.

Proposed resolution:

This wording is relative to N4981.

  1. Modify 25.7.18.3 [range.concat.iterator] as indicated:

    namespace std::ranges {
      template<input_range... Views>
        requires (view<Views> && ...) && (sizeof...(Views) > 0) &&
                  concatable<Views...>
      template<bool Const>
      class concat_view<Views...>::iterator {
    
      public:
        using iterator_category = see below;                                // not always present.
        using iterator_concept = see below;
        using value_type = concat-value-t<maybe-const<Const, Views>...>;
        using difference_type = common_type_t<range_difference_t<maybe-const<Const, Views>>...>;
    
      private:
        static constexpr bool concat-is-homogeneous = see below;                // exposition only
        using base-iter =                                                       // exposition only
          variant<iterator_t<maybe-const<Const, Views>>...>;
        
        […]
    
        friend constexpr void iter_swap(const iterator& x, const iterator& y) noexcept(see below)
          requires concat-is-homogeneous && 
                   indirectly_swappable<iterator_t<maybe-const<Const, Views...[0]>>>;
        friend constexpr void iter_swap(const iterator& x, const iterator& y) noexcept(see below)
          requires see below(!concat-is-homogeneous) &&
                   swappable_with<iter_reference_t<iterator>, iter_reference_t<iterator>> &&
                   (... && indirectly_swappable<iterator_t<maybe-const<Const, Views>>>);
      }
      […]
    }
    

    -?- concat-is-homogeneous is true if and only if the pack of types iterator_t<maybe-const<Const, Views>>... are all the same.

    -1- iterator::iterator_concept is defined as follows:

    […]

    friend constexpr void iter_swap(const iterator& x, const iterator& y) noexcept(see below)
      requires concat-is-homogeneous &&
               indirectly_swappable<iterator_t<maybe-const<Const, Views...[0]>>>;
    

    -?- Preconditions: x.it_.valueless_by_exception() and y.it_.valueless_by_exception() are each false.

    -?- Effects: Equivalent to:

    std::visit(ranges::iter_swap, x.it_, y.it_);
    

    -?- Remarks: The exception specification is equivalent to

    noexcept(ranges::iter_swap(std::get<0>(x.it_), std::get<0>(y.it_)))
    
    friend constexpr void iter_swap(const iterator& x, const iterator& y) noexcept(see below)
      requires see below(!concat-is-homogeneous) &&
               swappable_with<iter_reference_t<iterator>, iter_reference_t<iterator>> &&
               (... && indirectly_swappable<iterator_t<maybe-const<Const, Views>>>);
    

    -42- Preconditions: x.it_.valueless_by_exception() and y.it_.valueless_by_exception() are each false.

    -43- Effects: Equivalent to:

    std::visit([&](const auto& it1, const auto& it2) {
        if constexpr (is_same_v<decltype(it1), decltype(it2)>) {
          ranges::iter_swap(it1, it2);
        } else {
          ranges::swap(*x, *y);
        }
      },
      x.it_, y.it_);
    

    -44- Remarks: The exception specification is equivalent to

    (noexcept(ranges::swap(*x, *y)) && ... && noexcept(ranges::iter_swap(its, its)))
    

    where its is a pack of lvalues of type const iterator_t<maybe-const<Const, Views>> respectively.

    The expression in the requires-clause is equivalent to

    swappable_with<iter_reference_t<iterator>, iter_reference_t<iterator>> &&
    (... && indirectly_swappable<iterator_t<maybe-const<Const, Views>>>)