This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of SG9 status.

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

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

Priority: 2

View all other issues in [range.range].

View all issues with SG9 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.

[2025-10-23; Reflector poll; Status changed: New → SG9 and P2.]

This is certainly evoluationary question and should go to LEWG/SG9. It would disallow having unrelated begin/end members, where the range interface is provided by hidden friends instead of those members.

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

    6. (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.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.

    7. (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);
        } ; 
    

    -2- […]

    -3- […]

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