Document #: | P1306R4 [Latest] [Status] |
Date: | 2025-04-24 |
Project: | Programming Language C++ |
Audience: |
CWG |
Reply-to: |
Dan Katz <dkatz85@bloomberg.net> Andrew Sutton <andrew.sutton@beyondidentity.com> Sam Goodrick <samuel.goodrick@beyondidentity.com> Daveed Vandevoorde <daveed@edg.com> Barry Revzin <barry.revzin@gmail.com> |
This revision: Rewrote the prose and the wording.
[P1306R3] Expansion over a range requires a constant expression. Added support for break and continue control flow during evaluation.
[P1306R2] Adoption of template for
syntax. Added support for init-statement, folded pack expansion into new
expansion-init-list mechanism. Updated reflection code to match P2996.
Minor updates to wording: updated handling of switch statements, work
around lack of general non-transient constexpr allocation, eliminated
need for definition of an “intervening statement”, rebased onto working
draft, updated feature macro value, fixed typos. Addressed CWG review
feedback
[P1306R1] Adopted a unified syntax for
different forms of expansion statements. Further refinement of semantics
to ensure expansion can be supported for all traversable sequences,
including ranges of input iterators. Added discussion about
break
and
continue
within expansions.
[P1306R0] superceded and extended [P0589R0] (Tuple-based for loops) to work with more destructurable objects (e.g., classes, parameter packs). Added a separate constexpr-for variant that a) makes the loop variable a constant expression in each repeated expansion, and b) makes it possible to expand constexpr ranges. The latter feature is particularly important for static reflection.
This paper proposes a new kind of statement that enables the
compile-time repetition of a statement for each element of a tuple,
array, class, range, or brace-delimited list of expressions. Existing
methods for iterating over a heterogeneous container inevitably leverage
recursively instantiated templates to allow some part of the repeated
statement to vary (e.g., by type or constant) in each instantiation.
While such behavior can be encapsulated in a single library operation
(e.g., Boost.Hana’s for_each
) or,
potentially in the future, using the [:expand(...):]
construct built on top of [P2996R10] (Reflection for C++26)
reflection facilities, there are several reasons to prefer language
support:
First, repetition is a fundamental building block of algorithms, and should be expressible directly without complex template instantiation strategies.
Second, such repetition should be as inexpensive as possible. Recursively instantiating templates generates a large number of specializations, which can consume significant compilation time and memory resources.
Third, library-based approaches rely on placing the repeated
statements in a lambda body, which changes the semantics of something
like a
return
statement — and makes coroutines unusable.
Lastly, “iteration” over destructurable classes effectively requires language support to implement correctly.
Here are some basic usage examples:
Today
|
Proposed
|
---|---|
|
|
|
|
|
|
For the last row, expand
is
demonstrated in [P2996R10], define_static_array()
comes from [P3491R1]
(define_static_{string,object,array}) (although can be
implemented purely on top of p2996) and works around non-transient
allocation (more on this later), and nsdms(type)
is just shorthand for nonstatic_data_members_of(type, std::meta::access::unprivileged())
just to help fit.
The proposed design allows iterating over:
The expansion statement
template for (init-statementopt for-range-declaration : expansion-initializer) statement
will determine an expansion size based on the
expansion-initializer
and
then expand into:
{ init-statementopt// depends on expansion kind additional-expansion-declarationsopt; { = get-expr(0); for-range-declaration statement} { = get-expr(1); for-range-declaration statement} // ... repeated up to ... { = get-expr(expansion-size - 1); for-range-declaration statement} }
The mechanism of determining the
additional-expansion-declarations
(if any), the expansion size, and
get-expr
depends on the
expansion-initializer
.
If expansion-initializer
is of the form { expression-list }
,
then:
additional-expansion-declarations
expression
s in the
expression-list
(possibly
0), andget-expr(i)
is the i
th
expression
in the
expression-list
.For example:
Code
|
Expands Into
|
---|---|
|
|
Approximately anyway. The
expression-list
need not be
a simple pack expansion for which pack indexing applies, that’s just for
illustration purposes.
An earlier revision of this paper did not have dedicated syntax for expansion over packs. The syntax for the above example was originally proposed as:
template <typename... Ts> void print_all(Ts... elems) { template for (auto elem : elems) { // just elems ::println("{}", elem); std} }
This was pointed out by Richard Smith to be ambiguous on the EWG reflector. Consider:
template <typename... Ts> void fn(Ts... vs) { ([&](auto p){ template for (auto& v : vs) { // ... } }(vs), ...); }
Consider the call fn(array{1, 2, 3, 4}, array{1, 3, 5, 7}, array{2, 4, 6, 8})
.
It is far from clear whether the expansion statement containing
vs
expands over:
array
arguments (once for each invocation of the lambda), orint
elements
(of a different array
for each
invocation of the lambda).Initially, support for pack iteration was dropped from the proposal
entirely, but it was added back using the
expansion-init-list
syntax
in [P1306R2].
In addition to avoiding ambiguity, it is also broadly more useful
than simply expanding over a pack since it allows ad hoc expressions.
For instance, can add prefixes, suffixes, or even multiple packs: {0, xs..., 1, ys..., 2}
is totally fine.
If expansion-initializer
is a single expression that is a range, then:
addition-expansion-declarations
is:
auto&& __range = expansion-initializer;
constexpropt auto __begin = begin-expr; // see [stmt.ranged] constexpropt
where the
constexpr
specifier is present when the
for-range-declaration
is
declared with
constexpr
.
the expansion size is end-expr - __begin
.
This expression must be a constant expression. It is possible for this
to be the case even if
__begin
is not
constexpr
,
but expansion statements over ranges in general are really only useful
if the loop element is
constexpr
.
get-expr(i)
is *(__begin + i)
.
For example:
Code
|
Expands Into
|
---|---|
|
|
Note that the __range
variable is declared
constexpr
here. As such, all the usual rules for
constexpr
variables apply. Including the restriction on non-transient
allocation.
Consider:
template <typename T> void print_members(T const& v) { template for (constexpr auto r : nonstatic_data_members_of(^^T)) { ::println(".{}={}", identifier_of(r), v.[:r:]); std} }
Examples like this feature prominently in [P2996R10]. And at first glance, this
seems fine. The compiler knows the length of the vector returned by
members_of(^^T)
,
and can expand the body for each element. However, the expansion in
question more or less requires a constexpr vector, which the language is
not yet equipped to handle.
We at first attempted to carve out a narrow exception from
[expr.const] to permit non-transient constexpr allocation in this very
limited circumstance. Although the wording seemed reasonable, our
implementation experience with Clang left us less than optimistic for
this approach: The architecture of Clang’s constant evaluator really
does make every effort to prevent dynamic allocations from surviving the
evaluation of a constant expression (certainly necessary to produce a
“constexpr vector
”).
After some wacky experiments that amounted to trying to “rip the
constant evaluator in half” (i.e., separating the “evaluation state”,
whereby dynamically allocated values are stored, from the rest of the
metadata pertaining to an evaluation), we decided to fold: as of the
[P1306R3] revision, we instead propose
restricting expansion over iterable expressions to only cover those that
are constant expression.
In other words — the desugaring described above (which is similar to
the desugaring for the C++11 range-based
for
statement) — is what you get. No special cases.
Regrettably, this makes directly expanding over members_of(^^T)
ill-formed for C++26 – but all is not lost: By composing
members_of
with the
define_static_array
function from
[P3491R1]
(define_static_{string,object,array}) we obtain a
constexpr
span
containing the same reflections
from members_of
:
template <typename T> void print_members(T const& v) { template for (constexpr auto r : define_static_array(nonstatic_data_members_of(^^T))) { ::println(".{}={}", identifier_of(r), v.[:r:]); std} }
This works fine, since we no longer require non-transient allocation. We’re good to go.
This yields the same expressive power, at the cost of a few extra characters and a bit more memory that must be persisted during compilation. It’s a much better workaround than others we have tried (e.g., the expand template), and if (when?) WG21 figures out how to support non-transient constexpr allocation, the original syntax should be able to “just work”.
If expansion-initializer
is a single expression that is a range, then:
the expansion size is the structured binding size of the
expansion-initializer
(9.7 [dcl.struct.bind])
addition-expansion-declarations
is:
auto&& [__v0, __v0, ..., __vexpansion_size-1] = expansion-initializer; constexpropt
get-expr(i)
is __vi
if
either the referenced type is an lvalue reference or the
expansion-initializer
is an
lvalue. Otherwise, std::move(__vi)
.
For example:
Code
|
Desugars Into
|
---|---|
|
|
Most types can either be used as a range or destructured, but not
both. And even some that can be used in both contexts have equivalent
meaning in both — C arrays and
std::array
.
However, it is possible to have types that have different meanings with either interpretation. That means that, for a given type, we have to pick one interpretation. Which should we pick?
One such example is std::ranges::subrange(first, last)
.
This could be:
[first, last)
.first
and
last
(i.e. always size 2).Another such example is a range type that just happens to have all
public members. std::views::empty<T>
isn’t going to have any non-static data members at all, so it’s
tuple-like (with size 0) and also a range (with size 0), so that one
amusingly works out the same either way.
But any other range whose members happen to be public probably wants to be interpreted as a range. Moreover, the structured binding rule doesn’t actually require public members, just accessible ones. So there are some types that might be only ranges externally but could be both ranges and tuples internally.
In all of these cases, it seems like the obviously desired interpretation is as a range. Which is why we give priority to the range interpretation over the tuple interpretation.
Additionally, given a type that can be interpreted both ways, it easy enough to force the tuple interpretation if so desired:
template <class T> constexpr auto into_tuple(T const& v) { auto [...parts] = v; return std::tie(parts...); }
break
and
continue
Earlier revisions of the paper did not support
break
or
continue
within expansion statements. There was previously concern that users
would expect such statement to exercise control over the code generation
/ expansion process at translation time, rather than over the evaluation
of the statement.
Discussions with others have convinced us that this will not be an
issue, and to give the keywords their most obvious meaning:
break
jumps
to just after the end of the last expansion, whereas
continue
jumps to the start of the next expansion (if any).
There are regular requests to support expanding over types directly, rather than expressions:
template <typename... Ts> void f() { // strawman syntax template for (typename T : {Ts...}) { <T>(); do_something} }
Something like this would be difficult to support directly since you can’t tell that the declaration is just a type rather than an unnamed variable. But with Reflection coming, there’s less motivation to come up with a way to address this problem directly since we can just iterate in the value domain:
template <typename... Ts> void f() { template for (constexpr auto r : {^^Ts...}) { using T = [:r:]; <T>(); do_something} }
Bloomberg’s Clang/P2996 fork (available on Godbolt) implements all
features proposed by this paper. Expansion statements are enabled with
the -fexpansion-statements
flag (or with -freflection-latest
).
Update 6.4.2 [basic.scope.pdecl]/11 to specify the locus of an expansion statement:
11 The locus of a
for-range-declaration
of a range-basedfor
statement ([stmt.range]) is immediately after thefor-range-initializer
. The locus of afor-range-declaration
of an expansion statement ([stmt.expand]) is immediately after theexpansion-initializer
.
Update 6.4.3 [basic.scope.block]/1.1 to include expansion statements:
- (1.1) selection,
oriteration, or expansion statement ([stmt.select], [stmt.iter] , [stmt.expand])
Modify 6.7.7 [class.temporary]/5 to clarify that there are now six contexts:
5 There are
fivesix contexts in which temporaries are destroyed at a different point than the end of the full-expression. […]
Insert a new paragraph after 6.7.7 [class.temporary]/7 to extend the lifetime of temporaries created by expansion statements, and update the ordinal number used in paragraph 8:
7 The fourth context is when a temporary object other than a function parameter object is created in the
for-range-initializer
of a range-basedfor
statement. […]7+ The fifth context is when a temporary object other than a function parameter object is created in the
expansion-initializer
of an iterable or destructurable expansion statement, or in a full-expression in theexpansion-init-list
of an enumerated expansion statement ([stmt.expand]). If such a temporary object would otherwise be destroyed at the end of that full-expression, the object persists for the lifetime of the reference initialized by the expression in the expanded expansion statement.8 The
fifthsixth context is when a temporary object is created in a structured binding declaration ([dcl.struct.bind]). […]
Add a production for expansion statements to
statement
to 8.1 [stmt.pre]:
1 Except as indicated, statements are executed in sequence.
statement: labeled-statement attribute-specifier-seqopt expression-statement attribute-specifier-seqopt compound-statement attribute-specifier-seqopt selection-statement attribute-specifier-seqopt iteration-statement+ attribute-specifier-seqopt expansion-statement attribute-specifier-seqopt jump-statement declaration-statement attribute-specifier-seqopt try-block
Extend “substatement” to cover expansion statements in 8.1 [stmt.pre]/2:
2 A substatement of a
statement
is one of the following:
- (2.1) for a
labeled-statement
, itsstatement
,- (2.2) for a
compound-statement
, anystatement
of itsstatement-seq
,- (2.3) for a
selection-statement
, any of itsstatement
s orcompound-statement
s (but not itsinit-statement
),or- (2.4) for an
iteration-statement
, itsstatement
(but not aninit-statement
)., or- (2.5) for an
expansion-statement
, itsstatement
(but not aninit-statement
).
Extend “enclose” to cover expansion statements in 8.1 [stmt.pre]/3:
3 A
statement
S1
encloses astatement
S2
if
Add a new paragraph to the end of 8.2 [stmt.label]:
4 An identifier label shall not occur in an
expansion-statement
([stmt.expand]).
Insert this section after 8.6 [stmt.iter] (and renumber accordingly).
Expansion statements [stmt.expand]
1 Expansion statements specify repeated instantiations ([temp.spec]) of their substatement.
expansion-statement: template for ( init-statementopt for-range-declaration : expansion-initializer ) statement expansion-initializer: expression expansion-init-list expansion-init-list: { expression-list }
[ Note 1: Each
decl-specifier
in thedecl-specifier-seq
of afor-range-declaration
is either atype-specifier
orconstexpr
([stmt.ranged]). — end note ]2 The
statement
of anexpansion-statement
is a control-flow-limited statement ([stmt.label]).3 For the purpose of name lookup and instantiation, the
for-range-declaration
and thestatement
of theexpansion-statement
are together considered a template definition.4 An expression is iterable if, when treated as a
for-range-initializer
([stmt.iter.general]), the expressionsbegin-expr
andend-expr
can be determined as specified in [stmt.ranged], and, if they are of the formbegin(range)
andend(range)
, then argument-dependent lookup finds at least one function or function template for each.5 An expansion statement is
- (5.1) an iterable expansion statement if its
expansion-initializer
is an iterable expression that does not have array type;- (5.2) otherwise, a destructurable expansion statement if its
expansion-initializer
is a non-type-dependent expression with a structured binding size ([dcl.struct.bind]);- (5.3) otherwise, an enumerated expansion statement if its
expansion-initializer
is of the formexpansion-init-list
;- (5.4) otherwise, ill-formed.
6 An expansion statement
S
with anexpansion-initializer
E
is expanded unless either
- (6.1)
E
is type-dependent,- (6.2)
S
is an iterable expansion statement andE
is value-dependent ([temp.dep.constexpr]) or- (6.3)
S
is an enumerated expansion statement andE
contains a pack.7 An expanded expansion statement
S
is equivalent to acompound-statement
containing instantiations of thefor-range-declaration
(including its implied initialization), together with thestatement
; these instantiations correspond to expressions resulting from the analysis of theexpansion-initializer
as follows:
(7.1) If
S
is an iterable expansion statement, there is an instantiation for each element in the range computed by theexpansion-initializer
;S
is equivalent to:{ init-statementstatic constexpr auto&& range = expansion-initializer ; static constexpr auto begin = begin-expr; // see [stmt.ranged] static constexpr auto end = end-expr; // see [stmt.ranged] // ith repetition of the substatement { static constexpr auto iteri = begin + i; = *iteri; for-range-declaration statement} }
for all
i
in the range[0, N)
, forN
such thatbegin + N == end
. The variablesrange
,begin
,end
, anditeri
are variables defined for exposition only.[ Note 2: The instantiation is ill-formed if
range
is not a constant expression ([expr.const]) — end note ](7.2) Otherwise, if
S
is a destructurable expansion statement, the number of instantiations is equal to the structured binding size of theexpansion-initializer
;S
is equivalent to:{ init-statementstatic constexpropt auto&& seq = expansion-initializer ; { // ith repetition of the substatement = get-expri ; for-range-declaration statement} }
where
get-expri
would be theith
structured binding of the declarationauto&& [u0, u1, ..., un-1] = expansion-initializer ; .
The
constexpr
is present in the declaration ofseq
if and only ifconstexpr
appears in thefor-range-declaration
. The nameseq
is used for exposition only.(7.3) Otherwise (
S
is an enumerated expansion statement), there is an instantiation for each expression in theexpression-list
of theexpansion-init-list
;S
is equivalent to:{ init-statement{ // ith repetition of the substatement = get-expri ; for-range-declaration statement} }
where
get-expri
is the ithexpression
in theexpression-list
.[ Example 1:— end example ]struct S { int i; short s; }; consteval long f(S s) { long result = 0; template for (auto x : s) { += x; result } return result; } static_assert(f(S{1, 2}) == 3);
[ Example 2:— end example ]consteval int f(auto const&... Containers) { int result = 0; template for (auto const& c : {Containers...}) { += c[0]; result } return result; } constexpr int c1[] = {1, 2, 3}; constexpr int c2[] = {4, 3, 2, 1}; static_assert(f(c1, c2) == 5);
[ Editor's note: The following example assumes the changes proposed by P2996R11 and P3491R2. ]
[ Example 3:— end example ]template <typename T> consteval std::optional<int> f() { constexpr auto statics = std::define_static_array( ::meta::static_data_members_of( std^^T, ::meta::access_context::current())); stdtemplate for (constexpr std::meta::info s : statics) if (std::meta::identifier_of(s) == "ClsId") return [:s:]; return std::nullopt; } struct Cls { static constexpr int ClsId == 14; }; static_assert(f<Cls>().value() == 14);
Modify 8.7.2 [stmt.break]/1 to allow
break
in
expansion statements:
1 A
break
statement shall be enclosed by ([stmt.pre]) aniteration-statement
([stmt.iter]), anexpansion-statement
([stmt.expand]), or aswitch
statement ([stmt.switch]). Thebreak
statement causes termination of the smallest such enclosing statement; control passes to the statement following the terminated statement, if any.
[ Editor's note: We recommend the phrase “continuation portion” in lieu of “loop-continuation portion” to emphasize that an expansion statement is not a loop. ]
Modify 8.7.3 [stmt.cont]/1 to allow
continue
in
expansion statements:
1 Acontinue
statement shall be enclosed by ([stmt.pre]) aniteration-statement
([stmt.iter]) or anexpansion-statement
([stmt.expand]). Thecontinue
statement causes control to pass to theloopcontinuation portion of the smallest such enclosing statement, that is, to the end of the loop or expansion. More precisely, in each of the statements
while (foo) { { // ... } : ; contin}
do { { // ... } : ; contin} while (foo);
for (;;) { { // ... } : ; contin}
template for (auto e : foo) { { // ... } : ; contin}
a
continue
not contained in an enclosing iteration or expansion statement is equivalent togoto contin
.
Update the fallthrough attribute wording in 9.13.5 [dcl.attr.fallthrough]/1 to discuss expansion statements:
1 The
attribute-token
fallthrough
may be applied to a null statement; such a statement is a fallthrough statement. Noattribute-argument-clause
shall be present. A fallthrough statement may only appear within an enclosingswitch
statement ([stmt.switch]). The next statement that would be executed after a fallthrough statement shall be a labeled statement whose label is a case label or default label for the sameswitch
statement and, if the fallthrough statement is contained in an iteration statement or expansion statement, the next statement shall be part of the same execution of the substatement of the innermost enclosing iteration statement or the same expansion of the innermost enclosing expansion statement. The program is ill-formed if there is no such statement.
Update 13.8.1 [temp.res.general]/6.1 to permit early checking of expansion statements in dependent contexts.
6 The validity of a templated entity may be checked prior to any instantiation.
[ Note 3: Knowing which names are type names allows teh syntax of every template to be checked in this way. — end note ]
The program is ill-formed, no diagnostic required, if
(6.1) no valid specialization, ignoring
static_assert-declaration
s that fail ([dcl.pre]), can be generated for a templated entity or a substatement of a constexpr if statement ([stmt.if]) or expansion statement within a templated entity and the innermost enclosing template is not instantiated, or(6.2) […]
Add the following case to 13.8.3.3 [temp.dep.expr]/3 (and renumber accordingly):
3 An
id-expression
is type-dependent if it is atemplate-id
that is not a concept-id and is dependent; or if its terminal name is
- (3.1) […]
- (3.10) a
conversion-function-id
that specifies a dependent type, or- (3.10+) a name introduced by the
for-range-declaration
D
of an expansion statementS
ifD
contains a placeholder type and either- (3.11) dependent
or if it names […]
Add the following case to 13.8.3.4 [temp.dep.constexpr]/2 (and renumber accordingly):
2 An
id-expression
is value-dependent if
Add to 15.12 [cpp.predefined]:
__cpp_expansion_statements 2025XXL