Document #: | P1306R5 [Latest] [Status] |
Date: | 2025-06-20 |
Project: | Programming Language C++ |
Audience: |
CWG |
Reply-to: |
Dan Katz <[email protected]> Andrew Sutton <[email protected]> Sam Goodrick <[email protected]> Daveed Vandevoorde <[email protected]> Barry Revzin <[email protected]> |
[P1306R4] and R5: 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 destructuring 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) compound-statement
will determine an expansion size based on the
expansion-initializer
and
then expand into:
{ init-statementopt// depends on expansion kind additional-expansion-declarationsopt; { = E(0); for-range-declaration compound-statement} { = E(1); for-range-declaration compound-statement} // ... repeated up to ... { = E(expansion-size - 1); for-range-declaration compound-statement} }
The mechanism of determining the
additional-expansion-declarations
(if any), the expansion size, and
E
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 expansion-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 stop counting contexts:
5 There are
fiveseveral 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 is created in the
for-range-initializer
of either a range-based for statement or an enumerating expansion statement ([stmt.expand]). If such a temporary object would otherwise be destroyed at the end of thefor-range-initializer
full-expression
, the object persists for the lifetime of the reference initialized by thefor-range-initializer
.7+ The fifth context is when a temporary object is created in the
expansion-initializer
of an iterating or destructuring expansion statement. If such a temporary object would otherwise be destroyed at the end of thatexpansion-initializer
, the object persists for the lifetime of the reference initialized by theexpansion-initializer
, if any.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]. Also move the
grammar for
for-range-declaration
from
[stmt.iter.general] to here:
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 init-statement: expression-statement simple-declaration alias-declaration condition: expression $attribute-specifier-seqopt decl-specifier-seq declarator brace-or-equal-initializer structured-binding-declaration initializer + for-range-declaration: + attribute-specifier-seqopt decl-specifier-seq declarator + structured-binding-declaration + + for-range-initializer: + expr-or-braced-init-list
See [dcl.meaning] for the optional
attribute-specifier-seq
in afor-range-declaration
.
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
, itscompound-statement
(but not aninit-statement
).
Extend “enclose” to cover expansion statements in 8.1 [stmt.pre]/3:
3 A
statement
S1
encloses astatement
S2
if
Extend 8.1 [stmt.pre]/8 to cover
for-range-declaration
s:
8 In the
decl-specifier-seq
of acondition
or of afor-range-declaration
, including that of anystructured-binding-declaration
of thecondition
, eachdecl-specifier
shall either be atype-specifier
orconstexpr
. Thedecl-specifier-seq
of afor-range-declaration
shall not define a class or enumeration.
Add a new paragraph to the end of 8.2 [stmt.label]:
4 An identifier label shall not be enclosed by an
expansion-statement
([stmt.expand]).
Strike the productions for
for-range-declaration
and
for-range-initializer
from
[stmt.iter.general], as they’ve been moved to [stmt.pre]:
iteration-statement: while ( condition ) statement do statement while ( expression ) ; for ( init-statement conditionopt ; expressionopt ) statement for ( init-statementopt for-range-declaration : for-range-initializer ) statement - for-range-declaration: - attribute-specifier-seqopt decl-specifier-seq declarator - structured-binding-declaration - - for-range-initializer: - expr-or-braced-init-list
See [dcl.meaning] for the optionalattribute-specifier-seq
in afor-range-declaration
.
Strike 8.6.5 [stmt.ranged]/2, as it’s been integrated into [stmt.pre]/8.
2 In thedecl-specifier-seq
of afor-range-declaration
, eachdecl-specifier
shall be either atype-specifier
orconstexpr
. Thedecl-specifier-seq
shall not define a class or enumeration.
Insert this section after 8.6 [stmt.iter] (and renumber accordingly).
Expansion statements [stmt.expand]
1 Expansion statements specify repeated instantiations ([temp.decls.general]) of their substatement.
expansion-statement: template for ( init-statementopt for-range-declaration : expansion-initializer ) compound-statement expansion-initializer: expression expansion-init-list expansion-init-list: { expression-listopt }
2 The
compound-statement
of anexpansion-statement
is a control-flow-limited statement ([stmt.label]).3 For an expression
E
, let the expressionsbegin-expr
andend-expr
be determined as specified in [stmt.ranged]. An expression is expansion-iterable if it does not have array type and either
- (3.1)
begin-expr
andend-expr
are of the formE.begin()
andE.end()
or- (3.2) argument-dependent lookups for
begin(E)
and forend(E)
each find at least one function or function template.4 An expansion statement is
- (4.1) an enumerating expansion statement if its
expansion-initializer
is of the formexpansion-init-list
;- (4.2) otherwise, an iterating expansion statement if its
expansion-initializer
is an expansion-iterable expression;- (4.3) otherwise, a destructuring expansion statement.
5 An expansion statement
S
is equivalent to acompound-statement
containing instantiations of thefor-range-declaration
(including its implied initialization), together with thecompound-statement
ofS
, as follows:
(5.1) If
S
is an enumerating expansion statement,S
is equivalent to:{ init-statement S0 … SN-1}
where
N
is the number of elements in theexpression-list
,Si
is{ = Ei ; for-range-declaration compound-statement}
and
Ei
is the ith element of theexpression-list
.(5.2) Otherwise, if
S
is an iterating expansion statement,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] S0 …N-1 S}
where
N
is the result of evaluating the expression[] consteval { ::ptrdiff_t result = 0; stdfor (auto i = begin; i != end; ++i, ++result) ; return result; // distance from begin to end }()
and
Si
is{ static constexpr auto iter = begin + i; = *iter; for-range-declaration compound-statement}
The variables
range
,begin
,end
, anditer
are defined for exposition only.[ Note 1: The instantiation is ill-formed if
range
is not a constant expression ([expr.const]) — end note ](5.3) Otherwise,
S
is a destructuring expansion statement andS
is equivalent to:{ init-statementauto&& [u0, u1, …, uN-1] = expansion-initializer ; constexpropt S0 … SN-1}
where
N
is the structured binding size of the type of theexpansion-initializer
andSi
is{ = ui ; for-range-declaration compound-statement}
The keyword
constexpr
is present in the declaration ofu0, u1, …, uN-1
if and only ifconstexpr
is one of thedecl-specifier
s of thedecl-specifier-seq
of thefor-range-declaration
.[ Example 1:— end example ]consteval int f(auto const&... Containers) { int result = 0; template for (auto const& c : {Containers...}) { // OK, enumerating expansion statement += c[0]; result } return result; } constexpr int c1[] = {1, 2, 3}; constexpr int c2[] = {4, 3, 2, 1}; static_assert(f(c1, c2) == 5);
[ Example 2:— end example ]consteval int f() { constexpr std::array<int, 3> arr {1, 2, 3}; int result = 0; template for (constexpr int s : arr) { // OK, iterating expansion statement += sizeof(char[s]); result } return result; } static_assert(f() == 6);
[ Example 3:— end example ]struct S { int i; short s; }; consteval long f(S s) { long result = 0; template for (auto x : s) { // OK, destructuring expansion statement += sizeof(x); result } return result; } static_assert(f(S{}) == sizeof(int) + sizeof(short));
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 thesmallestinnermost 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 A
continue
statement shall be enclosed by ([stmt.pre]) aniteration-statement
([stmt.iter])or anexpansion-statement
. If the innermost enclosing such statementX
is aniteration-statement
([stmt.iter]), theThecontinue
statement causes control to pass to theloop continuation portion of the smallest such enclosing statement, that is, to the end of the loop. More precisely, in each of the statementsend of thestatement
orcompound-statement
ofX
. Otherwise, control passes to the end of thecompound-statement
of the currentSi
([stmt.expand]).
while (foo) { { // ... } contin: ; }
do { { // ... } contin: ; } while (foo);
for (;;) { { // ... } contin: ; }
acontinue
not contained in an enclosing iteration statement is equivalent togoto contin
.
Make a drive-by fix to paragraph 6 of 9.7 [dcl.struct.bind] to handle arrays of unknown bound:
6
E
shall not be an array type of unknown bound. IfE
isanany other array type with elementT
, the structured binding size ofE
is equal to the number of elements ofE
. Each SBi is the name of an lvalue that refers to the element i of the array and whose type isT
; the referenced type isT
.
Update the list of templated entities in 13.1 [temp.pre]/8:
8 An entity is templated if it is
- (8.1) a template,
- (8.2) an entity defined ([basic.def]) or created ([class.temporary]) within the
compound-statement
of an expansion statement ([stmt.expand]),- (8.3) an entity defined
([basic.def])or created([class.temporary])in a templated entity,- (8.4) a member of a templated entity,
- (8.5) an enumerator for an enumeration that is a templated entity, or
- (8.6) the closure type of a lambda-expression ([expr.prim.lambda.closure]) appearing in the declaration of a templated entity.
Add to 13.7.1 [temp.decls.general]/3:
3 […] For the purpose of instantiation, the substatements of a constexpr if statement are considered definitions. For the purpose of name lookup and instantiation, the
compound-statement
of theexpansion-statement
is considered a template definition.
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 the 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]) within a templated entity and the innermost enclosing template is not instantiated, or
- (6.x) no valid specialization, ignoring
static_assert-declaration
s that fail, can be generated for thecompound-statement
of an expansion statement and there is no instantiation of it, 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
N
introduced by thefor-range-declaration
D
of an expansion statementS
if the type specified forN
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
Define the point of instantiation for an expansion statement in a new paragraph at the end of 13.8.4.1 [temp.point]:
8 For the
compound-statement
of an expansion statement ([stmt.expand]), the point of instantiation is the point of instantiation of its enclosing templated entity, if any. Otherwise, it immediately follows the namespace-scope declaration or definition that contains the expansion statement.
Add to 15.12 [cpp.predefined]:
__cpp_expansion_statements 2025XXL