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.
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
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) — […]
(2.2) — […]
(2.3) — […]
(2.4) — […]
(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) — […]
(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) — […]
(2.2) — […]
(2.3) — […]
(2.4) — […]
(2.5) — […]
(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) — […]
(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.