Created on 2025-09-28.00:00:00 last changed 1 week ago
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 [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:
(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) — Otherwise, if `T` is an array type ([dcl.array]) and remove_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 to `t + 0`.
(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())`.
(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.
(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 ([basic.lookup.argdep]), then `ranges::begin(E)` is expression-equivalent to that expression.
(2.6) — Otherwise, `ranges::begin(E)` is ill-formed.
Modify [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:
(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) — Otherwise, if `T` is an array type ([dcl.array]) and remove_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 to t + extent_v<T>.
(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())`.
(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.
(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 ([basic.lookup.argdep]), then `ranges::end(E)` is expression-equivalent to that expression.
(2.7) — Otherwise, `ranges::end(E)` is ill-formed.
Modify [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) — both `ranges::begin(t)` and `ranges::end(t)` are specified to select `auto(t.begin())` and `auto(t.end())` respectively, or
(?.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)
Modify [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
(2.1) — […]
(2.2) — […]
(2.3) — […]
(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 ([stmt.ranged]) of that statement are semantically equivalent to auto begin = ranges::begin(t); and auto end = ranges::end(t); respectively.
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.
History | |||
---|---|---|---|
Date | User | Action | Args |
2025-10-03 13:25:11 | admin | set | messages: + msg15096 |
2025-09-28 00:00:00 | admin | create |