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
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.
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
E
with typeT
, lett
be an lvalue that denotes the reified object forE
. Then:
(2.1) — If
E
is an rvalue andenable_borrowed_range<remove_cv_t<T>>
isfalse
,ranges::begin(E)
is ill-formed.(2.2) — Otherwise, if
T
is 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
T
is 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 forbegin
in the scope of that class finds at least one declaration,ranges::begin(E)
is ill-formed.(2.5) — Otherwise, if
T
is a class or enumeration type andauto(begin(t))
is a valid expression whose type modelsinput_or_output_iterator
where the meaning ofbegin
is 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
E
with typeT
, lett
be an lvalue that denotes the reified object forE
. Then:
(2.1) — If
E
is an rvalue andenable_borrowed_range<remove_cv_t<T>>
isfalse
,ranges::end(E)
is ill-formed.(2.2) — Otherwise, if
T
is 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
T
is an array of unknown bound,ranges::end(E)
is ill-formed.(2.4) — Otherwise, if
T
is 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 forend
in the scope of that class finds at least one declaration,ranges::end(E)
is ill-formed.(2.6) — Otherwise, if
T
is 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 istrue
if and only if for thet
introduced 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
t
such thatdecltype((t))
isT&
,T
modelsrange
only if
(2.1) — […]
(2.2) — […]
(2.3) — […]
(2.?) — The range-based
for
statementfor (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.