Title
`ranges::for_each` possibly behaves differently from range-based `for`
Status
new
Section
[range.range]
Submitter
Jiang An

Created on 2025-09-28.00:00:00 last changed 1 week ago

Messages

Date: 2025-10-10.14:07:36

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 [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 ([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 ([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 [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 ([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 ([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 [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 [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 ([stmt.ranged]) of that statement are semantically equivalent to auto begin = ranges::begin(t); and auto end = ranges::end(t); respectively.

Date: 2025-09-28.00:00:00

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.

History
Date User Action Args
2025-10-03 13:25:11adminsetmessages: + msg15096
2025-09-28 00:00:00admincreate