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.

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-03

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) — […]

    2. (2.2) — […]

    3. (2.3) — […]

    4. (2.4) — […]

    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) — […]

    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) — […]

    2. (2.2) — […]

    3. (2.3) — […]

    4. (2.4) — […]

    5. (2.5) — […]

    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) — […]

    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.