do
expressions| Document #: | P2806R4 [Latest] [Status] |
| Date: | 2026-07-15 |
| Project: | Programming Language C++ |
| Audience: |
EWG |
| Reply-to: |
Bruno Cardoso Lopes <bruno.cardoso@gmail.com> Zach Laine <whatwasthataddress@gmail.com> Michael Park <mcypark@gmail.com> Barry Revzin <barry.revzin@gmail.com> |
do
expressions
Since [P2806R3], implementation,
wording, introducing implicit last value, and adding an optional
init-hoist to
do
expressions to address lifetime issues.
Since [P2806R2], wording and referencing a longer discussion on divergence in [P3549R0] (Diverging expressions).
Since [P2806R1], switched syntax
from
do return
to do_return to avoid ambiguity.
Added section on lifetime.
Since [P2806R0], some more discussion about implicit last value vs explicit return, reflection, and a grammar fix to the still-incomplete wording.
C++ is a language built on statements.
if is not an
expression, loops aren’t expressions, statements aren’t expressions
(except maybe in the specific case of
expression;).
When a single expression is insufficient, the only solution C++ currently has at its disposal is to invoke a function - where that function can now contain arbitrarily many statements. Since C++11, that function can be expressed more conveniently in the form of an immediately invoked lambda.
However, this approach leaves a lot to be desired. An immediately
invoked lambda introduced an extra function scope, which makes control
flow much more challenging - it becomes impossible to
break or
continue out
of a loop, and attempting to
return from
the enclosing function or
co_await,
co_yield, or
co_return
from the enclosing coroutine becomes an exercise in cleverness.
You also have to deal with the issue that the difference between
initializing a variable using an immediately-invoked lambda and
initializing a variable from a lambda only differs in the trailing
(),
arbitrarily deep into an expression, which are easy to forget. Some
people actually use
std::invoke
in this context, specifically to make it clearer that this lambda is,
indeed, intended to be immediately invoked.
This problem surfaces especially brightly in the context of [P2688R4] (Pattern Matching:
match Expression),
where the current design is built upon a sequence of:
pattern => expression;
This syntax only allows for a single
expression, which means
that pattern matching has to figure out how to deal with the situation
where the user wants to write more than, well, a single expression. The
current design is to allow
{ statement }
to be evaluated as an expression of type
void. This
is a hack, which is kind of weird (since such a thing is not actually an
expression of type
void), but
also limits the ability for pattern matching to support another kind of
useful syntax:
=> braced-init-list:
auto f() -> std::pair<int, int> { // this is fine return {1, 2}; // this is ill-formed in P1371 return true match -> std::pair<int, int> { _ => {1, 2} }; }
There’s no way to make that work, because
{ starts a
statement. So the choice in that paper lacks orthogonality: we have a
hack to support multiple expressions (which are very important to
support) that is inventing such support on the fly, in a novel way that
is very narrow (only supports
void), that
throws other useful syntax under the bus.
What pattern matching really needs here is a statement-expression syntax. But it’s not just pattern matching that has a strong desire for statement-expressions, this would be a broadly useful facility, so we should have an orthogonal language feature that supports statement-expressions in a way that would allow pattern matching to simplify its grammar to:
pattern => expr-or-braced-init-list;
Indeed, as of [P2688R5]
(Pattern Matching: `match` Expression), the
{ statement }
production is gone and the
braced-init-list case is
now supported. Pattern matching now relies upon
do
expressions to support multiple statements.
do
expressionsOur proposal is the addition of a new kind of expression, called a
do
expression.
In its simplest form:
int x = do { do_return 42; }; int y = do { 42 }; // equivalent to above
A do
expression consists of a sequence of statements, but is still, itself,
an expression (and thus has a value and a type). There are a lot of
interesting rules that we need to discuss about how those statements
behave.
A do
expression does introduce a new block scope - as the braces might
suggest. But it does not introduce a new function scope. There
is no new stack frame. Which is what allows external control flow to
work (see below).
do_return statementThe new do_return statement has
the same form as the
return
statement we have today:
do_return expr-or-braced-init-listopt;.
Its behavior corresponds closely to that of
return, in
unsurprising ways - do_return yields
from a do
expression in the same way that
return
returns from a function.
While
do_return value;
and
return value;
do look quite close together and mean fairly different things, the
leading do
we think should be sufficiently clear, and we think it is a good
spelling for this statement.
Other alternative spellings we’ve considered:
do return
(in the previous revision of this paper, which has an ambiguity with
do ... while
loops)do_yield (presented to EWG in
Issaquah as the initial pre-publication draft of this proposal)do yielddo break
(similarly to
return, we
are breaking out of this expression, but is less likely to conflict
since break
is less likely to be used than
return and
also the corresponding
break value;
is invalid today)=>
(or some other arrow, like
<- or
<=)Let’s take the example motivating case from [P2561R2] (A control flow operator) and compare implicit last expression to explicit return:
Implicit Last Value
|
Explicit Return
|
|---|---|
|
|
In the simple cases, implicit last value (on the left) will be
shorter than an explicit return (on the right). But implicit last value
is more limited. We cannot do early return (by design), which means that
a do
expression would not be able to return from a loop either. We would have
to extend the language to support
if
expressions, so that at the very least the first example above could be
made easier - which would add more complexity to the design.
Which is to say — the do_return
statement is still a valuable and necessary addition, given that we do
not have if
or loop expressions, and we are unlikely to add them.
However, for many common uses of
do
expressions, we don’t actually need early return, so paying the
syntactic cost seems unnecessary. Which is why we’re proposing both.
Note that Rust also allows both (you can label a block expression and
then break
out of it).
The expression
do { do_return 42; }
is a prvalue of type
int. We
deduce the type from all of the (non-discarded)
do_return statements, in the same
way that
auto return
type deduction works for functions and lambdas.
An explicit
trailing-return-type can be
provided to override this:
do -> long { do_return 42; }
If no (non-discarded) do_return
statement appears in the body of the
do
expression, or every (non-discarded)
do_return statement is of the form
do_return;,
then the expression is a prvalue of type
void.
Falling off the end of a
do
expression behaves like an implicit
do_return; -
if this is incompatible with the type of the
do
expression, the expression is ill-formed. This is the one key difference
with functions: this case is not undefined behavior. This will be
discussed in more detail later.
This makes the pattern matching cases [P2688R4] work pretty naturally:
P2688R4
|
Proposed
|
|---|---|
|
|
Here, the whole match expression
has type
void because
each arm has type
void because
none of the
do
expressions have a do_return
statement.
Yes, this requires an extra
do for each
arm, but it means we have a language that’s much easier to explain
because it’s consistent -
do { cout << "don't care"; }
is a void
expression in any context. We don’t have a
compound-statement that
happens to be a
void
expression just in this one spot.
P2688R4
|
Proposed
|
|---|---|
|
|
Here, the existing pattern matching cannot support a
braced-init-list because
{ is used
for the special
void-statement-case.
But if we had
do
expressions, the grammar of pattern matching can use
expr-or-braced-init-list in
the same way that we already do in many other places in the C++ grammar.
This example just works.
All the rules for initializing from
do
expression, and the way the expression that appears in a
do_return statement is treated, are
the same as what the rules are for
return.
Implicit move applies, for variables declared within the body of the
do
expression. In the following example,
r is an unparenthesized
id-expression that names an
automatic storage variable declared within the statement, so it’s
implicitly moved:
std::string s = do {
std::string r = "hello";
r += "world";
do_return r;
};Note that automatic storage variables declared within the function
that the do
expression appears, but not declared within the statement-expression
itself, are not implicitly moved (since they can be used
later).
In a regular function, there are four ways to escape the function scope:
return
statementthrowing
an exception[[noreturn]]
function,
std::abort()
and
std::unreachable()void)The same is true for coroutines, except substituting
return for
co_return
(and likewise falling off the end is undefined behavior if there is no
return_void()
function on the promise type).
For a do
expression, we have two different directions where we can escape (in a
non-exception,
non-[[noreturn]]
case): we either yield an expression, or we escape the outer
scope. That is, we can also:
return
from the enclosing function (or
co_return
from the enclosing coroutine)break or
continue
from the innermost enclosing loop (if any, ill-formed otherwise)Additionally, for point (4) while we could simply (for consistency) propagate the same rules for falling-off-the-end as functions, then lambdas (C++11), then coroutines (C++20), we would like to consider not introducing another case for undefined behavior here and enforcing that the user provides more information themselves.
That is, the rule we propose is that the implementation form a
control flow graph of the
do
expression and consider each one of the six escaping kinds described
above. All (non-discarded) do_return
statements (including the implicit
do_return;
introduced by falling off the end, if the implementation cannot prove
that it does not happen) need to either have the same type (if no
trailing-return-type) or be
compatible with the provided return type (if provided). Anything else is
ill-formed.
Let’s go through some examples.
| Example | Discussion |
|---|---|
|
OK: All yielding control paths have the same type. There’s no falling off the end. |
|
Ill-formed: The yielding control paths have different types and there is
no provided
trailing-return-type. This
would be okay if it were
do -> int { ... }
or
do -> double { ... }
or
do -> float { ... },
etc.
|
|
OK: Similar to a, all yielding
control paths yield the same type. There is no falling off the end here,
it is not important that a yielding
if has an
else.
|
|
Ill-formed: There are two yielding control paths here: the
do_return 1;
and the implicit
do_return;
from falling off the end, those types are incompatible. The equivalent
in functions and coroutines would be undefined behavior if
cond is
false.
|
|
OK: As above, there are two yielding control paths here, but both the
explicit and the implicit ones are
do_return;
which are compatible.
|
|
OK: We no longer fall off the end here, since we always escape. There is only one yielding path. |
|
OK: Similar to the above, it’s just that we’re escaping by returning from the outer function instead of throwing. Still not falling off the end. |
|
OK:
std::abort()
means that we cannot fall off the end, see discussion on
[[noreturn]]
below.
|
|
Ill-formed: This is probably the most interesting case when it comes to
falling off the end. Here, the user knows that
c only has three values, but the
implementation does not, so it could still fall off the end. gcc does
warn on the equivalent function form of this, clang does not. The
typical solution here might be to add
__builtin_unreachable(),
now
std::unreachable(),
to the end of the function, but for this to work we have to discuss
[[noreturn]]
below. Barring that, the user would have to add either some default
value or some other kind of control flow (like an exception, etc).
|
|
OK: The first
for (;;),
then the lack of
do_return 2;
would be fine - but anything more complicated than that would require
some kind of final yield (or
throw, etc.)
|
To reiterate: the implementation produces a control flow graph of the
do
expression and considers all yielding statements (including the
implicit
do_return;
on falling off the end, if the implementation considers that to be a
possible path) in order to determine correctness of the
statement-expression. The kinds of control flow that escape the
statement entirely (exceptions,
return,
break,
continue,
co_return)
do not need to be considered for purposes of consistency of yields
(since they do not yield values).
noreturn functionsThe language currently has several kinds of escaping control flow
that it recognizes. As mentioned, exceptions,
return,
continue,
break, and
co_return.
And, allegedly,
goto.
But there’s one kind of escaping control flow that it does
not currently recognize: functions marked
[[noreturn]].
A call to
std::abort()
or
std::terminate()
or
std::unreachable()
escapes control flow, for sure, but today this is just an attribute:
int i = do { if (cond) { do_return 5; } std::abort(); // we know control flow never gets here, so we should not need to // insert an implicit "do_return;" };
Pattern Matching has this same problem - it needs to support arms
that might
std::terminate()
or are
std::unreachable(),
so that proposal currently is introducing a dedicated syntax to mark an
arm as non-returning:
!{ std::terminate(); }.
Which is… less than ideal.
However, the rule in 9.13.10 Noreturn attribute [dcl.attr.noreturn]/2 is:
2 If a function
fis called wherefwas previously declared with thenoreturnattribute andfeventually returns, the behavior is undefined.
That is normative wording which we can rely on. The above
do
expression can only fall off the end if
std::abort
returns, which is already undefined behavior. We can avoid
introducing any new undefined behavior ourselves as part of this
feature.
That is: invoking a function marked
[[noreturn]]
can be considered an escaping control flow in exactly the same way that
return,
break,
throw, etc.,
are already.
Note that this violates the so-called Second Ignorability Rule suggested in [P2552R2] (On the ignorability of standard attributes), which is a great reason to ignore that rule.
Consider:
int i = do -> int { throw 42; };
This is weird, but might end up as a result of template instantiation
where maybe other control paths (guarded with an
if constexpr)
actually had do_return statements in
them. So it needs to be allowed.
It does lead to an interesting question: what is
decltype(do { return; })?
We already define
decltype(throw 42)
to be void,
so would this also be
void? It’s
kind of an odd choice, and it would be nice if we had a specific type
for an escaping expression. While we could come up with the right
language facility to allow the conditional operator
(?:) and
pattern matching to work correctly by ignoring arms that are
always-escaping, user-defined code would have no way of differentiating
between a real void expression
(std::print("Hello {}!", "EWG")
is actually an expression of type
void) and an
artificial one
(std::abort()
is not really the same thing).
We could instead introduce a new type,
std::noreturn_t
(as an easier-to-type spelling of
⊥), change
decltype(throw e)
to be
std::noreturn_t
(since nobody actually writes this - code search results are exclusively
in compiler test suites) and treat the return types of
[[noreturn]]
functions as
std::noreturn_t.
Then the type system gains understanding of always-escaping
expressions/statements and the rules for pattern matching, the
conditional operator,
do
expressions, and arbitrary user-defined libraries just fall out.
See reflector discussion here and more thorough discussion in [P3549R0] (Diverging expressions).
gotoUsing
goto in a
do
expression has some unique problems.
Jumping within a
do
expression should follow whatever restrictions we already have (see
8.10 Declaration statement [stmt.dcl]). Jumping
into a
do
expression should be completely disallowed (we would call the
statement of a
do
expression a control-flow limited statement).
Jumping out of a
do
expression is potentially useful though, in the same way that
break,
continue,
and return
are:
for (loop1) { for (loop2) { int i = do { if (cond) { goto done; } do_return value; }; } } done:
Breaking out of multiple loops is one of the uses of
goto that
has no real substitute today. The above example should be fine. But
referring to any label that is in scope of the variable we’re
initializing needs to be disallowed - since we wouldn’t have actually
initialized the variable. We need to ensure that the 8.10
Declaration statement [stmt.dcl] rule is
extended to cover this case.
Also, while computed goto is not a standard C++ feature, it would be
nice to disallow this example, courtesy of (of course) JF Bastien (in
this case, we are referring to a label that is within
v’s scope. We’re not jumping to it
directly, but the ability to jump to it indirectly is still
problematic):
#include <stdio.h> struct label { static inline void* e; int v; label() try : v(({ fprintf(stderr, "oh\n"); e = &&awesome; throw 1; 42; })) { fprintf(stderr, "no\n"); awesome: fprintf(stderr, "you\n"); } catch(...) { fprintf(stderr, "don't\n"); goto *e; } }; int main() { label l; }
Consider:
int i = do { if (cond) { do_return 0; } };
Is this statement ill-formed (because there is a control path that
falls off the end of the
do
expression, as discussed in this section) or should this statement be
undefined behavior? The latter would be consistent with functions,
lambdas, and coroutines (and not an if-you-squint-enough kind of
consistency either, this would be exactly identical).
It would make for a simpler design if we adopted undefined behavior here, but we think it’s a better design to force the user to cover all control paths themselves.
One important question is: when are local variables declared within a
do
expression destroyed?
Let’s start with this example:
int i = do { std::lock_guard _(mtx); do_return get(0); } + do { std::lock_guard _(mtx); do_return get(1); };
Each do
expression is locking the same mutex,
mtx. We believe that it is the
overwhelming presumption of C++ that both
lock_guards will be destroyed at
their nearest
} (i.e. this
will not deadlock). There really is no other reasonable alternative.
However, consider this example, courtesy of Lauri Vasama in the
context of discussing lifetime questions in control flow operator ([P2561R2]). Consider this
set of functions, where
find_interesting returns some
span into the given
vector (or returns an
unexpected):
auto get_data() -> std::vector<int>; auto find_interesting(std::vector<int> const&) -> std::expected<std::span<int const>, std::string>; auto best_of(std::span<int const>) -> int;
With do
expressions, it would be tempting to implement a
TRY macro similar to Rust’s
try!
macro. In this case, we could try to do it this way (note that in this
case decaying is irrelevant, so we just return everything by value):
#define TRY(expr) do { \ auto __r = expr; \ if (not __r) { \ return std::unexpected(__r.error()); \ } \ do_return *__r; \ } auto do_something() -> std::expected<int, std::string> { int value = best_of(TRY(find_interesting(get_data()))); return value; }
This looks reasonable. Sure,
get_data()
is a temporary vector, but it
looks like it persists until the
;. But
that’s not what would actually happen. If we expand the macro, and add
the explicit return type for clarity, this evaluates as:
auto do_something() -> std::expected<int, std::string> { int value = best_of(do -> std::span<int const> { auto __r = find_interesting(get_data()); // ^ // vector destroyed here if (not __r) { return std::unexpected(__r.error()); } do_return *__r; }); return value; }
The temporary
std::vector<int>
doesn’t persist through the whole
do
expression, it gets destroyed too soon — so our
__r would be holding a dangling
span (in the non-error case).
That’s… bad.
So we need some way to “persist” that temporary through the end of the outer expression. And it definitely cannot happen automatically, as just pointed out, so it has to be explicit in some way. We can think of three approaches to doing this:
__r somehow, such that any
temporaries in its initializer persist through the outer
full-expression.do
expression with some kind of anti-capture list, as in
do [__r=e] { ... }do
expression itself to carry a sequence of init-statements expressly for
this purpose.Having an annotation on a variable retroactively change its lifetime just doesn’t seem like a great idea. For the other two, the comparison looks like this:
Init-Hoist
|
Init-Statement
|
|---|---|
|
|
Both syntaxes would provide declarations whose lifetime extends to
the end of the full-expression containing the
do
expression. Any temporaries in those declarations’ initializers are
likewise extended to that full-expression. Which means that in this
case, the temporary
get_data()
will not be inside of a new full-expression (the body of the
do
expression), it will be in the place where it needs to be. We think this
is the right approach to addressing lifetime issues, in a way that
likely scales better.
Between these, we prefer the init-hoist approach (so named because
it’s kind of the opposite of an init-capture), since only one form of
init-statement even makes sense here. The downside is that we
would need
auto&&
semantics here rather than
auto
semantics which lambda init-capture has. It’s inconsistent, but with
very strong motivation for the difference — since lambdas can actually
escape but
do
expressions cannot.
The proposed form works.
The primary motivation for having an init-hoist is largely around
being able to define macros for expressions in ways that actually work
properly and to be able to desugar the control flow operator ([P2561R2]) into a
do
expression.
But, interestingly enough, pattern matching ([P2688R5]) offers a different way to solve both problems that wouldn’t need such a feature:
Init-Hoist
|
Pattern Matching
|
|---|---|
|
|
On the right, expr is obviously
evaluated outside of the
do
expression, and so any temporaries within it obviously last until the
end of the full-expression.
The question is: do we need to add an init-hoist feature for
do
expressions (a feature in no small part motivated by pattern matching)
if pattern matching could solve it for us?
We think it’s probably a good hedge to do it anyway. We’ll probably
ship do
expressions first, so the pattern matching paper can simply remove
it.
An interesting sub-question on lifetimes is what does this do:
auto prvalue() -> T; auto f() -> void { // lifetime extension, reference bound to temporary T const& r1 = prvalue(); // dangling T const& r2 = []() -> T const& { return prvalue(); }(); // lifetime extension?? T const& r3 = do -> T const& { do_return prvalue(); }; }
r1 is our familiar lifetime
extension case - a reference is bound to a temporary.
r2 is definitely dangling. The
temporary is destroyed and definitely does not last as long as
r2. But what about
r3, is it more like
r1 or
r2?
In this case, we see the entire
do
expression - so unlike the general callable case, it may actually be
possible for lifetime extension to work here.
But as we’re thinking about it, let’s make the example slightly more complicated:
auto lvalue() -> T const&; auto prvalue() -> T; auto g(bool c) -> void { T const& r4 = do -> T const& { if (c) { do_return lvalue(); } else { do_return prvalue(); } }; T const& r5 = do -> T const& { if (c) { do_return lvalue(); } else { T x = prvalue(); do_return x; } }; }
If r3 doesn’t dangle (and we do
lifetime extension), does r4? Well,
presumably. But here we have a form of runtime-conditional lifetime
extension. That’s still seemingly doable - we would effectively have an
optional<T> __storage
that is declared before r4 and then
r4 is either a reference into that
or whatever we got from
lvalue().
But even if we made r4 work,
r5 now almost certainly cannot - now
we’re definitely not binding a temporary to a reference, this is quite
adrift from our usual rules.
Which makes us wonder if there’s really any value in being
adventurous here - and instead probably consider that there is no
lifetime extension in any of these cases, not in
r3, not in
r4, and definitely not in
r5.
The only part of a
do
expression that is in the immediate context of a template substitution
is the trailing-return-type (if any). Any alternative choice would
require SFINAE on statements, which the language does not currently
support and we are not trying to tackle in this paper.
This is consistent with lambdas, where the lambda body is not in the immediate context.
We have to disambiguate between a
do
expression and a
do-while
loop.
In an expression-only context, the latter isn’t possible, so we’re fine there.
In a statement context, a
do
expression is completely pointless - you can just write statements. So
we disambiguate in favor of the
do-while
loop. If somebody really, for some reason, wants to write a
do
expression statement, they can parenthesize it:
(do { do_return 42; });.
A statement that begins with a
( has to
then be an expression, so we’re now in an expression-only context.
A previous iteration of the paper used
do return
(with a space), which would lead to an ambiguity in the following in the
context of a
do
expression:
do return value; while (cond);
This could have been parsed as a
do return
statement followed by an infinite loop (that would never be executed
because we’ve already returned out of the expression) or as a
do-while
loop containing a single, unbraced, return statement. If we use the
do_return spelling, there’s no such
ambiguity, since the above can only be a
do ... while
loop.
Also because we would unconditionally parse a statement as beginning
with do as a
do-while
loop, code like this would not work:
do { do_return X{}; }.foo();
Such code would also have to be parenthesized to disambiguate, which doesn’t seem like a huge burden on the user.
GCC has had an extension called statement-expressions for decades, which look very similar to what we’re proposing here:
gcc
|
Proposed
|
|---|---|
|
|
The reason we’re not simply proposing to standardize the existing extension is that there are two features we see that are lacking in it that are not easy to add:
if, due to
the implicit nature of the yield.For (1), there is simply no obvious place to put the
trailing-return-type. For
(2), you can’t turn
ifs into
expressions in any meaningful way. It is fairly straightforward to
answer both questions for our proposed form.
With allowing implicit last value,
the simple translation from gcc statement-expression to
do
expression is just a matter of swapping parentheses for a leading
do (and
dropping the
; on the
last statement). We’re just generalizing to make the feature more
flexible.
A question that often comes up, for any language feature: if we had reflection and, in particular, code injection: would we need this facility?
The answer is not only yes, but reflection is a good motivating use-case for this facility. Because the language does not have any kind of block expression today, adding support for one would increase the amount of ways that code injection could work.
One example might be, again, the control flow operator proposal in
[P2561R2] (A control flow
operator). If reflection allows me to write a hygienic
macro that does code injection, perhaps we could write a library such
that
try_(E)
would inject an expression that would evaluate in the way that that
paper proposes. But in order to do such a thing, we would need to be
able to have a block expression to inject. This paper provides such a
block expression.
do
expressions appeargcc’s statement-expressions are not usable in all expression contexts. Trying to use them at namespace-scope, or in a default member initializer, etc, fails:
int i = ({ // error: statement-expressions are not allowed outside functions int j = 2; // nor in template-argument lists j; });
In such contexts, there is a much smaller difference between a statement-expression and an immediately invoked lambda since you don’t have any other interesting control flow that you can do - the expression either yields a value or the program terminates.
But if we’re going to add a new language feature, it seems better to allow it to be used in all expression contexts - we would just have to say what happens in this case. Especially since if we’re adding a feature to subsume immediately invoked lambdas, it would be preferable to subsume all immediately invoked lambdas, not just some or most.
We can think of a
do
expression as simply behaving like an immediately invoked lambda in such
contexts. Not in the sense of allowing
return
statements (there’s still no enclosing function to return out of), but
the sense that any local variables declared would exist in a function
stack. But this is probably more of a compiler implementation detail
rather than a language design detail.
In short:
do
expressions should be usable in any expression context.
This is implemented in clang and can be seen on compiler explorer.
This example shows the combination of a
do
expression with an init-hoist and
TRY macro with the correct lifetime
semantics.
Here is a
more involved example showing a more complicated
TRY macro illustrating that clang
can still warn on dangling references in the right places.
Add the keyword do_return to the
table of keywords in 5.12 Keywords [lex.key]:
constinit const_cast continue contract_assert co_await co_return co_yield decltype default delete do + do_return double dynamic_cast else enum explicit export
Add a note to 6.10.1 Sequential execution [intro.execution]:
5 A full-expression is
- (5.1) an unevaluated operand ([expr.context]),
- (5.2) a
constant-expression([expr.const]),- (5.3) an immediate invocation ([expr.const]),
- (5.4) an
init-declarator([dcl.decl]) (including such introduced by a structured binding ([dcl.struct.bind])) or amem-initializer([class.base.init]), including the constituent expressions of the initializer,- (5.5) an invocation of a destructor generated at the end of the lifetime of an object other than a temporary object ([class.temporary]) whose lifetime has not been extended,
- (5.6) the predicate of a contract assertion ([basic.contract]), or
- (5.7) an expression that is not a subexpression of another expression and that is not otherwise part of a full-expression.
[…]
[ Note 1: An expression
Ein thecompound-statementof ado-expressionD([expr.prim.do]) can still be a full-expression even thoughDitself is an expression, becauseEis not a subexpression ofD. By contrast, the constituent expressions of aninitializerin theinit-hoist-introducerofDare part of the full-expression that containsD. — end note ]
Add do-expression to the
grammar in 7.5.1 Grammar [expr.prim.grammar]:
primary-expression: literal this ( expression ) id-expression lambda-expression fold-expression requires-expression splice-expression + do-expression
Extend the rule for move-eligible expressions in 7.5.5.2 Unqualified names [expr.prim.id.unqual]:
15 An implicitly movable entity is a variable with automatic storage duration that is either a non-volatile object or an rvalue reference to a non-volatile object type. An
id-expressionorsplice-expression([expr.prim.splice]) is move-eligible if
- (15.1) it designates an implicitly movable entity,
- (15.2) it is the (possibly parenthesized) operand of a
return([stmt.return])or,co_return([stmt.return.coroutine]), ordo_return([stmt.do.return]) statement, or of athrow-expression([expr.throw]), and- (15.3) each intervening scope between the declaration of the entity and the innermost enclosing scope of the expression is a block scope and, for a
throw-expression, is not the block scope of atry-blockorfunction-try-block, and- (15.4) for a
do_returnstatement, the entity belongs to the block scope of thecompound-statementof its associateddo-expression([stmt.do.return]), or to a block scope contained by that block scope.
Add a new clause [expr.prim.do] after 7.5.8.5 Nested requirements [expr.prim.req.nested]:
Do expressions [expr.prim.do]
1 A do-expression provides a way to combine multiple statements into a single expression without introducing a new function scope. A
do_returnstatement ([stmt.do.return]) produces the result of thedo-expression. Other jump statements can transfer control out of ado-expressionwithout producing a value.[ Example 1:— end example ]constexpr int f(int i) { int half = do { if (i % 2 != 0) { return -1; // returns from f } do_return i / 2; // produces the value of the do-expression }; return half; } static_assert(f(5) == -1); static_assert(f(4) == 2);do-expression: do init-hoist-introduceropt trailing-return-typeopt compound-statement init-hoist-introducer: [ init-hoist-list ] init-hoist-list: init-hoist init-hoist-list , init-hoist init-hoist: identifier initializerA
do-result-expressionin thecompound-statementof ado-expressionis treated as ado_returnstatement associated with thatdo-expression([stmt.do.return]) and with an operand that is theexpression.[ Example 2:— end example ]int a = do { 42 }; // OK, equivalent to do { do_return 42; } int b = do { int x = 2; x + 3 }; // OK, equivalent to do { int x = 2; do_return x + 3; } int c = do [x=2] { x + 3 }; // OK, init-hoist followed by implicit do_return2 The
init-hoist-introducer, if any, of ado-expressionallows declarations to be introduced within an expression. Ado-expressionwith aninit-hoist-introducerintroduces a block scope ([basic.scope.block]) that includes theinit-hoist-listand thecompound-statement. Eachinit-hoistdeclares a variable; its type is deduced as if from a declaration of the formauto&& init-hoist ;([dcl.spec.auto]), and the variable is so initialized. The point of declaration of theidentifierof aninit-hoistis immediately after itsinitializer([basic.scope.pdecl]). The variables declared by theinit-hoist-listare initialized in order before thecompound-statementis executed.3 The constituent expressions of the
initializerof aninit-hoistare part of the full-expression ([intro.execution]) that contains thedo-expression. [ Note 1: Consequently, a temporary created during the initialization of such a variable is destroyed at the end of that full-expression ([class.temporary]), not at the end of thedo-expression. — end note ]4 A
do_returnstatement ([stmt.do.return]) whose associateddo-expressionhas aninit-hoist-introducerdoes not exit the block scope introduced by thatinit-hoist-introducer. The variables declared in theinit-hoist-listare destroyed, in reverse order of their construction, at the end of the full-expression that contains thedo-expression; if control instead exits that block scope by any other means, those variables are destroyed as that scope is exited ([stmt.jump]). [ Note 2: Because each such variable has reference type, a temporary bound to it ([class.temporary]) is likewise destroyed at the end of the full-expression that contains thedo-expression. — end note ][ Example 3:— end example ]constexpr int f() { return do [x = 1, y = 2] { x + y }; } static_assert(f() == 3); // OK constexpr int const& id(int const& r) { return r; } constexpr int const* ptr(int const& r) { return &r; } constexpr int h() { return do [p = ptr(id(42))] { *p }; } static_assert(h() == 42); // OK, no dangling5 The
compound-statementof ado-expressionis a control-flow-limited statement ([stmt.label]).6 A
returnstatement ([stmt.return]) appearing in ado-expressiontransfers control to the caller of the function that lexically contains thedo-expression. Aco_return,co_await, orco_yieldstatement or expression appearing in ado-expressionacts on the coroutine that lexically contains thedo-expression. [ Note 3: That is, ado-expressionis transparent to the enclosing function or coroutine. — end note ]7 A
breakorcontinuestatement appearing in ado-expressionrefers to an enclosingiteration-statementorswitchstatement that contains thedo-expression. [ Note 4: Abreakorcontinuestatement cannot be used to exit ado-expressionitself. — end note ]8 A
do-expressionthat is not within a function shall not contain areturnstatement, aco_returnstatement, aco_awaitexpression, or aco_yieldexpression.9 A
gotostatement ([stmt.goto]) shall not transfer control into ado-expressionfrom outside thatdo-expression.10 The type
DO-TYPEof ado-expressionis determined as follows:
- (10.1) If there is a
trailing-return-typethat does not contain a placeholder type ([dcl.spec.auto]), thenDO-TYPEis the type specified by thattrailing-return-type.- (10.2) Otherwise,
DO-TYPEis deduced from the non-discardeddo_returnstatements associated with thedo-expression:
- (10.2.1) If there is no non-discarded
do_returnstatement associated with thedo-expression(including nodo-result-expression), or every non-discardeddo_returnstatement associated with thedo-expressionhas no operand,DO-TYPEisvoid.- (10.2.2) Otherwise,
DO-TYPEis deduced as if from areturnstatement using the rules in [dcl.spec.auto.general]. All non-discardeddo_returnstatements associated with thedo-expressionshall deduce to the same type; otherwise, the program is ill-formed.[ Example 4:— end example ]auto a = do { do_return 1; }; // OK, deduces int auto b = do -> long { do_return 1; }; // OK, explicit type is long auto c = do { // error: inconsistent deduction if (cond) do_return 1; do_return 2.0; };11 The type and value category of the
do-expressionare determined fromDO-TYPEas follows:
- (11.1) If
DO-TYPEisT&, thedo-expressionis an lvalue of typeT.- (11.2) Otherwise, if
DO-TYPEisT&&, thedo-expressionis an xvalue of typeT.- (11.3) Otherwise, the
do-expressionis a prvalue of typeDO-TYPE.12 If control can flow off the end of the
compound-statementof ado-expressionwhose type is notcv void, the program is ill-formed. [ Note 5: Control flows off the end if there exists any path through the compound statement that does not terminate with ado_returnstatement, athrowexpression, a call to a[[noreturn]]function, or a jump statement that exits thedo-expression. — end note ][ Example 5:— end example ]extern bool cond; void f() { auto a = do { // error: control may flow off the end if (cond) do_return 1; }; auto b = do { // OK, all paths yield or exit if (cond) do_return 1; throw 2; }; auto c = do { // OK, return exits the do-expression if (cond) do_return 1; return; }; (do -> void { // OK, void type allows flow off the end if (cond) do_return; }); }13 The initialization of the returned reference or prvalue result object of a
do-expressionis sequenced before the destruction of temporaries at the end of the full-expression established by the operand of the executeddo_returnstatement, which, in turn, is sequenced before the destruction of local variables ([stmt.jump]) whose scope is exited by thedo_returnstatement. [ Note 6: A copy or move operation associated with ado_returnstatement can be elided or converted to a move operation the same as for areturnstatement ([class.copy.elision]). — end note ]14 A
do-expressioncan appear in any context where an expression is permitted, including at namespace scope. [ Note 7: At namespace scope or in a default member initializer, there is no enclosing function, soreturn,break,continue, and coroutine statements/expressions cannot be used. — end note ]
Change 8.1 Preamble [stmt.pre] to make the
compound-statement of a
do-expression transparent
to the enclosing statement:
3 A
statementS1encloses astatementS2if
- (3.1)
S2is a substatement ofS1,- (3.2)
S1is aselection-statement,iteration-statement, orexpansion-statement, andS2is theinit-statementofS1,- (3.3)
S1is atry-blockandS2is itscompound-statementor any of thecompound-statements of itshandlers,or- (3.4)
S1encloses a statementS3andS3enclosesS2., or- (3.5)
S2is thecompound-statementof ado-expressionE([expr.prim.do]),Eappears withinS1, andEdoes not appear within an interveninglambda-expressionor function body.
Change 8.3 Expression statement [stmt.expr] to disambiguate
a do-expression from a
do-while
loop:
1 Expression statements have the form
expression-statement: expressionopt;The expression is a discarded-value expression ([expr.context]). All side effects from an expression statement are completed before the next statement is executed. An expression statement with the expression missing is called a null statement. The
expressionshall not be ado-expression.[ Note 1: A statement beginning with the keyword
dois always parsed as ado-whilestatement ([stmt.do]). Ado-expressionused as a statement must be parenthesized. — end note ]
Change 8.4 Compound statement or block [stmt.block] to allow
a compound-statement to end
in an expression in the context of a
do-expression:
1 A compound statement (also known as a block) groups a sequence of statements into a single statement.
compound-statement: { statement-seqopt label-seqopt } + { statement-seqopt do-result-expression } +do-result-expression: + expressionA label at the end of a compound-statement is treated as if it were followed by a null statement.
* A
do-result-expressionshall appear only in thecompound-statementof ado-expression([expr.prim.do]).2 [ Note 1: A compound statement defines a block scope ([basic.scope]). — end note ]
Add to the grammar in 8.8.1 General [stmt.jump.general]:
1 Jump statements unconditionally transfer control.
jump-statement: break ; continue ; return expr-or-braced-init-listopt ; + do_return expr-or-braced-init-listopt ; coroutine-return-statement goto identifier ;2 […]
Change 8.8.2 The break statement [stmt.break] to prohibit
its use in do-expressions
in confusing places:
1 A
breakstatement shall be enclosed by ([stmt.pre]) aniteration-statement([stmt.iter]), anexpansion-statement([stmt.expand]), or aswitchstatement ([stmt.switch]). Thebreakstatement causes termination of the innermost such enclosing statement; control passes to the statement following the terminated statement, if any.2 A
breakstatement shall not appear in ado-expression([expr.prim.do]) that is a subexpression of:
- (2.1) the
init-statement,condition, orexpressionof aforstatement ([stmt.for]),- (2.2) the
conditionof awhilestatement ([stmt.while]),- (2.3) the
init-statement,for-range-declaration, orfor-range-initializerof a range-basedforstatement ([stmt.ranged]),- (2.4) the
expressionof adostatement ([stmt.do]), or- (2.5) the
init-statementorconditionof aswitchstatement ([stmt.switch])unless the enclosing
iteration-statementorswitchstatement of thatbreakstatement is also within the samedo-expression.
Change 8.8.3 The continue statement [stmt.cont] similarly:
1 A
continuestatement shall be enclosed by ([stmt.pre]) aniteration-statement([stmt.iter]) or anexpansion-statement([stmt.expand]). […]2 A
continuestatement shall not appear in ado-expression([expr.prim.do]) that is a subexpression of:
- (2.1) the
init-statement,condition, orexpressionof aforstatement ([stmt.for]),- (2.2) the
conditionof awhilestatement ([stmt.while]),- (2.3) the
init-statement,for-range-declaration, orfor-range-initializerof a range-basedforstatement ([stmt.ranged]), or- (2.4) the
expressionof adostatement ([stmt.do])unless the enclosing
iteration-statementof thatcontinuestatement is also within the samedo-expression.
Add a new subclause [stmt.do.return] “The
do_return statement” after
8.8.5 The co_return statement [stmt.return.coroutine]:
The
do_returnstatement [stmt.do.return]1 A
do_returnstatement shall appear only within thecompound-statementof ado-expression([expr.prim.do]), and not within an interveninglambda-expressionor function body. The innermost suchdo-expressionis thedo_returnstatement’s associateddo-expression.[ Example 1:— end example ]int f() { return do { auto g = []() { do_return 1; // error: crosses lambda boundary }; do_return 2; // OK }; }2 The
expr-or-braced-init-listof ado_returnstatement is called its operand. Ado_returnstatement with no operand shall be used only if its associateddo-expressionhas typecv void. Ado_returnstatement with an operand of typevoidshall be used only if its associateddo-expressionhas typecv void. Ado_returnstatement with any other operand shall be used only if its associateddo-expressionhas type other thancv void; thedo_returnstatement initializes the returned reference or prvalue result object of its associateddo-expressionby copy-initialization ([dcl.init]) from the operand.3 A
do_returnstatement that binds
- (3.1) a returned reference or
- (3.2) a constituent reference ([intro.object]) of a returned object
to a temporary expression ([class.temporary]) is ill-formed.
[ Example 2:— end example ]int& f(); auto a = do -> int const& { do_return f(); }; // OK auto b = do -> int const& { do_return 42; }; // error: binds reference to temporary
Add to 15.12 Predefined macro names [cpp.predefined]:
+ __cpp_do_expressions 20XXXXL
match Expression.