4389. ranges::for_each possibly behaves differently from range-based for

Section: 25.4.2 [range.range] Status: New Submitter: Jiang An Opened: 2025-09-28 Last modified: 2025-10-10

Priority: Not Prioritized

View all other issues in [range.range].

View all issues with New status.

Discussion:

It was found in the blog post "When ranges::for_each behaves differently from for" that ranges::for_each can behave differently from range-based for, because

  1. ranges::begin and ranges::end possibly use different rules, i.e. one calls a member and the other calls an ADL-found non-member function, and

  2. these CPOs continue to perform ADL when a member begin/end is found but the function call is not valid, while the range-for stops and renders the program ill-formed.

Perhaps the intent of Ranges was that the ranges::range concept should be stricter than plain range-for and all range types can be iterated via range-for with the same semantics as ranges::for_each. However, it seems very difficult (if not impossible) for a library implementation to tell whether a class has member begin/end but the corresponding member call is ill-formed with C++20 core language rules, and such determination is critical for eliminating the semantic differences between ranges::for_each and range-for.

Proposed resolution:

This wording is relative to N5014.

Two mutually exclusive resolutions are proposed here. One enforces semantic-identity checks, while the other doesn't and makes weird types satisfy but not model the range concept. I prefer the stricter one because the semantic-identity checks are fully static, but this probably requires compilers to add new intrinsics when reflection is absent.

Option A: (stricter)

  1. Modify 25.3.2 [range.access.begin] as indicated:

    -2- Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:

    1. (2.1) — If E is an rvalue and enable_borrowed_range<remove_cv_t<T>> is false, ranges::begin(E) is ill-formed.

    2. (2.2) — Otherwise, if T is an array type (9.3.4.5 [dcl.array]) and remove_all_extents_t<T> is an incomplete type, ranges::begin(E) is ill-formed with no diagnostic required.

    3. (2.3) — Otherwise, if T is an array type, ranges::begin(E) is expression-equivalent to t + 0.

    4. (2.4) — Otherwise, if auto(t.begin()) is a valid expression whose type models input_or_output_iterator, ranges::begin(E) is expression-equivalent to auto(t.begin()).

    5. (2.?) — Otherwise, if remove_cvref_t<T> is a class type and search for begin in the scope of that class finds at least one declaration, ranges::begin(E) is ill-formed.

    6. (2.5) — Otherwise, if T is a class or enumeration type and auto(begin(t)) is a valid expression whose type models input_or_output_iterator where the meaning of begin is established as-if by performing argument-dependent lookup only (6.5.4 [basic.lookup.argdep]), then ranges::begin(E) is expression-equivalent to that expression.

    7. (2.6) — Otherwise, ranges::begin(E) is ill-formed.

  2. Modify 25.3.3 [range.access.end] as indicated:

    -2- Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:

    1. (2.1) — If E is an rvalue and enable_borrowed_range<remove_cv_t<T>> is false, ranges::end(E) is ill-formed.

    2. (2.2) — Otherwise, if T is an array type (9.3.4.5 [dcl.array]) and remove_all_extents_t<T> is an incomplete type, ranges::end(E) is ill-formed with no diagnostic required.

    3. (2.3) — Otherwise, if T is an array of unknown bound, ranges::end(E) is ill-formed.

    4. (2.4) — Otherwise, if T is an array, ranges::end(E) is expression-equivalent to t + extent_v<T>.

    5. (2.5) — Otherwise, if auto(t.end()) is a valid expression whose type models sentinel_for<iterator_t<T>> then ranges::end(E) is expression-equivalent to auto(t.end()).

    6. (2.?) — Otherwise, if remove_cvref_t<T> is a class type and search for end in the scope of that class finds at least one declaration, ranges::end(E) is ill-formed.

    7. (2.6) — Otherwise, if T is a class or enumeration type and auto(end(t)) is a valid expression whose type models sentinel_for<iterator_t<T>> where the meaning of end is established as-if by performing argument-dependent lookup only (6.5.4 [basic.lookup.argdep]), then ranges::end(E) is expression-equivalent to that expression.

    8. (2.7) — Otherwise, ranges::end(E) is ill-formed.

  3. Modify 25.4.2 [range.range] as indicated:

    -1- […]

    template<class T>
      concept range =
        requires(T& t) {
          ranges::begin(t);    // sometimes equality-preserving (see below)
          ranges::end(t);
        } && has-consistent-begin-end<T>; // see below
    

    -2- […]

    -3- […]

    -?- has-consistent-begin-end<T> is a constant expression of type bool, and it is true if and only if for the t introduced in the requires-expression above, either

    1. (?.1) — both ranges::begin(t) and ranges::end(t) are specified to select auto(t.begin()) and auto(t.end()) respectively, or

    2. (?.2) — both ranges::begin(t) and ranges::end(t) are specified not to select auto(t.begin()) and auto(t.end()) respectively.

Option B: (looser)

  1. Modify 25.4.2 [range.range] as indicated:

    -1- […]

    template<class T>
      concept range =
        requires(T& t) {
          ranges::begin(t);    // sometimes equality-preserving (see below)
          ranges::end(t);
        }
    

    -2- Given an expression t such that decltype((t)) is T&, T models range only if

    1. (2.1) — […]

    2. (2.2) — […]

    3. (2.3) — […]

    4. (2.?) — The range-based for statement for (auto&& x: t); is well-formed, and variable definitions auto begin = begin-expr; and auto end = end-expr; in the equivalent form (8.6.5 [stmt.ranged]) of that statement are semantically equivalent to auto begin = ranges::begin(t); and auto end = ranges::end(t); respectively.