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.

4218. Constraint recursion in basic_const_iterator's relational operators due to ADL + CWG 2369

Section: 24.5.3.5 [const.iterators.ops] Status: New Submitter: Patrick Palka Opened: 2025-03-03 Last modified: 2025-03-09

Priority: Not Prioritized

View all issues with New status.

Discussion:

Consider the example (devised by Hewill Kang)

using RCI = reverse_iterator<basic_const_iterator<vector<int>::iterator>>;
static_assert(std::totally_ordered<RCI>);

Checking RCI is totally_ordered entails checking

requires (RCI x) { x RELOP x; } for each RELOP in {<, >, <=, >=}

which we expect to be straightforwardly satisfied by reverse_iterator's namespace-scope operators (24.5.1.8 [reverse.iter.cmp]):

template<class Iterator1, class Iterator2>
  constexpr bool operator<(
    const reverse_iterator<Iterator1>& x,
    const reverse_iterator<Iterator2>& y);
// etc

But due to ADL we find ourselves also considering the basic_const_iterator relop friends (24.5.3.5 [const.iterators.ops]/24).

template<input_iterator Iterator>
class basic_const_iterator {
  template<not-a-const-iterator I>
    friend constexpr bool operator<(const I& x, const basic_const_iterator& y)
      requires random_access_iterator<Iterator> && totally_ordered_with<Iterator, I>
  // etc
};

Before CWG 2369 these candidates would quickly get discarded since for the second operand RCI clearly isn't convertible to basic_const_iterator. But after CWG 2369 implementations must first check these operators' constraints (with Iterator = vector<int>::iterator and I = RCI), which entails checking totally_ordered<RCI> recursively, causing the example to be ill-formed.

The constraint recursion is diagnosed by GCC (See godbolt demo). Other compilers accept the example because they don't implement CWG 2369, as far as I know.

GCC trunk works around this issue by giving these friend relational operators a dependent second operand of the form basic_const_iterator<J> where J is constrained to match Iterator:

template<not-a-const-iterator I, same_as<Iterator> J>
  friend constexpr bool operator<(const I& x, const basic_const_iterator<J>& y)
    requires random_access_iterator<Iterator> && totally_ordered_with<Iterator, I>
// etc

So that deduction fails earlier, before constraints get checked, for a second operand that isn't a specialization of basic_const_iterator (or derived from one).

LWG 3769(i) is an earlier issue about constraint recursion in basic_const_iterator's operators, but there the recursion was independent of CWG 2369.

Proposed resolution:

This wording is relative to N5001.

  1. Modify 24.5.3.3 [const.iterators.iterator], class template basic_const_iterator synopsis, as indicated:

    namespace std {
      […]
      template<input_iterator Iterator>
      class basic_const_iterator {
        […]
        template<not-a-const-iterator I, same_as<Iterator> J>
          friend constexpr bool operator<(const I& x, const basic_const_iterator<J>& y)
            requires random_access_iterator<Iterator> && totally_ordered_with<Iterator, I>;
        template<not-a-const-iterator I, same_as<Iterator> J>
          friend constexpr bool operator>(const I& x, const basic_const_iterator<J>& y)
            requires random_access_iterator<Iterator> && totally_ordered_with<Iterator, I>;
        template<not-a-const-iterator I, same_as<Iterator> J>
          friend constexpr bool operator<=(const I& x, const basic_const_iterator<J>& y)
            requires random_access_iterator<Iterator> && totally_ordered_with<Iterator, I>;
        template<not-a-const-iterator I, same_as<Iterator> J>
          friend constexpr bool operator>=(const I& x, const basic_const_iterator<J>& y)
            requires random_access_iterator<Iterator> && totally_ordered_with<Iterator, I>;
        […]
      };
    }
    
  2. Modify 24.5.3.5 [const.iterators.ops] as indicated:

    template<not-a-const-iterator I, same_as<Iterator> J>
      friend constexpr bool operator<(const I& x, const basic_const_iterator<J>& y)
        requires random_access_iterator<Iterator> && totally_ordered_with<Iterator, I>;
    template<not-a-const-iterator I, same_as<Iterator> J>
      friend constexpr bool operator>(const I& x, const basic_const_iterator<J>& y)
        requires random_access_iterator<Iterator> && totally_ordered_with<Iterator, I>;
    template<not-a-const-iterator I, same_as<Iterator> J>
      friend constexpr bool operator<=(const I& x, const basic_const_iterator<J>& y)
        requires random_access_iterator<Iterator> && totally_ordered_with<Iterator, I>;
    template<not-a-const-iterator I, same_as<Iterator> J>
      friend constexpr bool operator>=(const I& x, const basic_const_iterator<J>& y)
        requires random_access_iterator<Iterator> && totally_ordered_with<Iterator, I>;
    

    -23- Let op be the operator.

    -24- Effects: Equivalent to: return x op y.current_;