Title
Ambiguity of `std::ranges::advance` and `std::ranges::next` when the difference type is also a sentinel type
Status
ready
Section
[range.iter.op.advance][range.iter.op.next]
Submitter
Jiang An

Created on 2026-01-09.00:00:00 last changed 1 week ago

Messages

Date: 2026-02-27.17:32:50

Proposed resolution:

This wording is relative to N5032.

  1. Modify [iterator.concept.sentinel] as indicated:

    template<class S, class I>
      concept sentinel_for =
        semiregular<S> &&
        !is-integer-like<S> &&
        input_or_output_iterator<I> &&
        weakly-equality-comparable-with<S, I>; // see [concept.equalitycomparable]
    
Date: 2026-02-15.00:00:00

[ 2026-02-27; LWG telecon: move to Ready ]

Date: 2026-02-15.00:00:00

[ 2026-02-20; Jonathan provides new wording ]

Date: 2026-02-15.00:00:00

[ 2026-02-20; Reflector poll. ]

Set priority to 3 after reflector poll.

The type in the example is basically a `std::any` that additionally type-erases equality. That causes a problem for templated iterators that have their value type's namespace as an associated namespace for ADL.

Instead of allowing integers to be sentinels in general but just restricting them from being used with these overloads, we could just change the concept so that integers do not model `sentinel_for`. That's similar to what we did for `span`'s constructor which has a similar problem (though that one's more aggressive due to compatibility constraints).

This wording is relative to N5032.

  1. Modify [iterator.synopsis], header <iterator> synopsis, as indicated:

    […]
    namespace std {
      template<class T> using with-reference = T&; // exposition only
      template<class T> concept can-reference      // exposition only
        = requires { typename with-reference<T>; };
      template<class T> concept dereferenceable    // exposition only
        = requires(T& t) {
          { *t } -> can-reference; // not required to be equality-preserving
        };
      
      template<class T>
        constexpr bool is-integer-like = see below;           // exposition only
    
      […]
      // [range.iter.ops], range iterator operations
      namespace ranges {
        // [range.iter.op.advance], ranges::advance
        template<input_or_output_iterator I>
          constexpr void advance(I& i, iter_difference_t<I> n);                // freestanding
        template<input_or_output_iterator I, sentinel_for<I>class S>
          requires (!is-integer-like<S>) && sentinel_for<S, I>
          constexpr void advance(I& i, S bound);                               // freestanding
        template<input_or_output_iterator I, sentinel_for<I> S>
          constexpr iter_difference_t<I> advance(I& i, iter_difference_t<I> n, // freestanding
                                                 S bound);    
       […]
        // [range.iter.op.next], ranges::next
        template<input_or_output_iterator I>
          constexpr I next(I x);                                  // freestanding
        template<input_or_output_iterator I>
          constexpr I next(I x, iter_difference_t<I> n);          // freestanding
        template<input_or_output_iterator I, sentinel_for<I>class S>
          requires (!is-integer-like<S>) && sentinel_for<S, I>
          constexpr I next(I x, S bound);                         // freestanding
        template<input_or_output_iterator I, sentinel_for<I> S>
          constexpr I next(I x, iter_difference_t<I> n, S bound); // freestanding    
      }
      […]
    }
    
  2. Modify [range.iter.op.advance] as indicated:

    template<input_or_output_iterator I, sentinel_for<I>class S>
      requires (!is-integer-like<S>) && sentinel_for<S, I>
      constexpr void advance(I& i, S bound);
    

    -3- Preconditions: […]

  3. Modify [range.iter.op.next] as indicated:

    template<input_or_output_iterator I, sentinel_for<I>class S>
      requires (!is-integer-like<S>) && sentinel_for<S, I>
      constexpr I next(I x, S bound);
    

    -3- Effects: […]

Date: 2026-01-09.00:00:00

Currently, `ranges::advance` and `ranges::next` have `operator()` overloads that accept `(iterator, sentinel)` and `(iterator, difference)`. However, when the difference type of the iterator is also a sentinel type, there may be ambiguity in both overloads.

E.g. the following example is rejected when compiling with libc++ or libstdc++ (demo):

#include <cstddef>
#include <iterator>
#include <type_traits>

template<class T>
struct FwdIter { // triggers ADL for T
  FwdIter();

  using value_type      = std::remove_cv_t<T>;
  using difference_type = int;

  T& operator*() const;

  FwdIter& operator++();
  FwdIter operator++(int);

  friend bool operator==(const FwdIter&, const FwdIter&);
};

static_assert(std::forward_iterator<FwdIter<int>>);

struct OmniConv {
  OmniConv(const auto&);
  friend bool operator==(OmniConv, OmniConv); // found by ADL via things related to OmniConv
};

int main() {
  FwdIter<OmniConv> it{};
  std::ranges::advance(it, 0); // ambiguous
  std::ranges::next(it, 0); // ambiguous
}

Perhaps it would be better to ensure that calling `ranges::advance` or `ranges::next` with an iterator value and a value of the difference type is unambiguous. Note that wrapping iterators that trigger ADL for the value type like the `FwdIter` in this example are common in standard library implementations, so ambiguity can be easily raised from containers with such `OmniConv` being their element type.

History
Date User Action Args
2026-02-27 17:32:50adminsetmessages: + msg15990
2026-02-27 17:32:50adminsetstatus: new -> ready
2026-02-20 20:39:01adminsetmessages: + msg15965
2026-02-20 20:39:01adminsetmessages: + msg15964
2026-01-18 12:36:04adminsetmessages: + msg15889
2026-01-09 00:00:00admincreate