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.
ranges::for_each possibly behaves differently from range-based forSection: 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
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
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)
Modify 25.3.2 [range.access.begin] as indicated:
-2- Given a subexpression
Ewith typeT, lettbe an lvalue that denotes the reified object forE. Then:
(2.1) — If
Eis an rvalue andenable_borrowed_range<remove_cv_t<T>>isfalse,ranges::begin(E)is ill-formed.(2.2) — Otherwise, if
Tis an array type (9.3.4.5 [dcl.array]) andremove_all_extents_t<T>is an incomplete type,ranges::begin(E)is ill-formed with no diagnostic required.(2.3) — Otherwise, if
Tis an array type,ranges::begin(E)is expression-equivalent tot + 0.(2.4) — Otherwise, if
auto(t.begin())is a valid expression whose type modelsinput_or_output_iterator,ranges::begin(E)is expression-equivalent toauto(t.begin()).(2.?) — Otherwise, if
remove_cvref_t<T>is a class type and search forbeginin the scope of that class finds at least one declaration,ranges::begin(E)is ill-formed.(2.5) — Otherwise, if
Tis a class or enumeration type andauto(begin(t))is a valid expression whose type modelsinput_or_output_iteratorwhere the meaning ofbeginis established as-if by performing argument-dependent lookup only (6.5.4 [basic.lookup.argdep]), thenranges::begin(E)is expression-equivalent to that expression.(2.6) — Otherwise,
ranges::begin(E)is ill-formed.
Modify 25.3.3 [range.access.end] as indicated:
-2- Given a subexpression
Ewith typeT, lettbe an lvalue that denotes the reified object forE. Then:
(2.1) — If
Eis an rvalue andenable_borrowed_range<remove_cv_t<T>>isfalse,ranges::end(E)is ill-formed.(2.2) — Otherwise, if
Tis an array type (9.3.4.5 [dcl.array]) andremove_all_extents_t<T>is an incomplete type,ranges::end(E)is ill-formed with no diagnostic required.(2.3) — Otherwise, if
Tis an array of unknown bound,ranges::end(E)is ill-formed.(2.4) — Otherwise, if
Tis an array,ranges::end(E)is expression-equivalent tot + extent_v<T>.(2.5) — Otherwise, if
auto(t.end())is a valid expression whose type modelssentinel_for<iterator_t<T>>thenranges::end(E)is expression-equivalent toauto(t.end()).(2.?) — Otherwise, if
remove_cvref_t<T>is a class type and search forendin the scope of that class finds at least one declaration,ranges::end(E)is ill-formed.(2.6) — Otherwise, if
Tis a class or enumeration type andauto(end(t))is a valid expression whose type modelssentinel_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]), thenranges::end(E)is expression-equivalent to that expression.(2.7) — Otherwise,
ranges::end(E)is ill-formed.
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 typebool, and it istrueif and only if for thetintroduced in the requires-expression above, either
(?.1) — both
ranges::begin(t)andranges::end(t)are specified to selectauto(t.begin())andauto(t.end())respectively, or(?.2) — both
ranges::begin(t)andranges::end(t)are specified not to selectauto(t.begin())andauto(t.end())respectively.
Option B: (looser)
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
tsuch thatdecltype((t))isT&,Tmodelsrangeonly if
(2.1) — […]
(2.2) — […]
(2.3) — […]
(2.?) — The range-based
forstatementfor (auto&& x: t);is well-formed, and variable definitionsauto begin = begin-expr;andauto end = end-expr;in the equivalent form (8.6.5 [stmt.ranged]) of that statement are semantically equivalent toauto begin = ranges::begin(t);andauto end = ranges::end(t);respectively.