Full name of submitter: Artyom Kolpakov
Consider the semantics of await-expression in [expr.await]
On the one hand, the standard in [expr.await]/3, [expr.await]/4, [expr.await]/5 and [intro.execution]/12 treats an await-expression as a single expression. Its subexpressions (await-ready, await-suspend, await-resume) are part of a single evaluation, and the result of the await-expression is the result of the await-resume expression. Thus, the await-expression follows the usual rules of expression evaluation, including sequencing, lifetime of temporaries, and exception propagation.
On the other hand, [expr.await]/5 specifies the semantics of an await-expression in terms of a sequence of conditionally evaluated subexpressions and associated effects, including a potential suspension and subsequent resumption of the coroutine. This introduces an observable discontinuity in execution, even though the await-expression remains a single evaluation in the abstract machine.
As I read [expr.await]/5, the evaluation proceeds as follows.
First, the await-ready expression is evaluated. If its evaluation (or the evaluation of preceding subexpressions of the await-expression) exits via an exception, the coroutine is not suspended, and the exception is thrown in the coroutine context from the await-expression. Otherwise, if the result of await-ready is true, the coroutine is not suspended, and the await-resume expression is evaluated as the result of the await-expression. Otherwise, the result of await-ready is false, and the coroutine is suspended (formally, considered suspended) in the middle of the await-expression evaluation, but control has not yet been transferred to the caller or resumer of the coroutine.
Now await-suspend is evaluated. If its evaluation exits via an exception, the exception is caught, the coroutine is resumed, and the exception is rethrown in the coroutine context from the await-expression, without the evaluation of the await-resume expression. Otherwise, await-suspend completes normally, control is transferred to the caller or resumer of the coroutine, regardless of the result of await-suspend, and the point of its suspension determines suspend point.
If the await-suspend returns false or handle h that refers to the coroutine, the coroutine is immediately resumed implicitly or via h.resume(), respectively. Otherwise, if await-suspend returns the handle h that refers to other coroutine, that coroutine resumed via h.resume(). Otherwise, control is "finally" transferred to the caller or resumer of the coroutine (exiting a coroutine call or a resume call).
And when the coroutine is resumed, the await-resume expression is evaluated.
It is possible to distinguish several stages in the described semantics of a single await-expression:
- the coroutine is already considered suspended, while the evaluation of the
await-expression continues (and the evaluation of the await-suspend begins);
- after the evaluation of
await-suspend, control may be transferred to the caller or resumer, and a suspend point is established.
Thus, the evaluation of the await-expression can be suspended and resumed later. At the same time, the standard permits resuming the coroutine via handle that refers to it during the evaluation of the await-suspend, that is, between stages and before the suspend point is established.
The interaction between the fact that the await-expression is a single evaluation, the staged structure of its specified semantics, and the possibility of resumption during await-suspend is not fully specified. This leads to a number of subtle issues.
Consider the following awaiter:
struct Awaiter {
auto await_ready() { return false; }
auto await_suspend(std::coroutine_handle<> h) { h.resume(); }
auto await_resume() { return 0; }
};
Although simple, it already reveals a number of problems. Since the coroutine is resumed before the await-suspend evaluation completes, there is no control transfer to caller or resumer of the coroutine between its suspension and resumption, which is inconsistent with [expr.await]/1.sentence-2. This also means that there is no established suspend point when resuming. The control transfer and the establishment of the suspend point will occur later, in fact, when it is no longer necessary. And consistency is lost with the initial suspend point and the final suspend point
A simple example:
auto res = co_await Awaiter{};
It demonstrates receiving the result of the await-expression before its evaluation completes. When the await-suspend resumes the coroutine before its completion, the await-resume evaluation already begins, although the evaluation of the await-expression will not end with it. Moreover, in this way the evaluation of the await-expression extends beyond its full-expression.
Another example:
(co_await Awaiter{})[ptr];
It shows a violation of the ordering. According to [expr.sub]/1, the await-expression should be fully evaluated before evaluation of the ptr. However, due to the resumption from within the await-suspend, the await-expression has not yet completed evaluation. And again, the result of the await-expression is observed before it evaluation completes.
Yet another example:
co_await Awaiter{};
co_await Awaiter{};
// ...
co_await Awaiter{};
It results in overlapping evaluation of consecutive await-expressions. Each subsequent await-expression starts to be evaluated before the previous one is evaluated. This leads to the existence of several await-expressions being evaluated at the same time, which destroys the sequential execution model ([intro.execution]).
More fundamentally, explicit resumption of the coroutine from within the await-suspend does not follow the usual linear call/return execution model and introduces re-entrant execution of the coroutine. Each such resumption may result in additional nested activations associated with the same await-expression, occurring after the coroutine is considered suspended. At the same time, from within the coroutine itself, execution continues to appear as a regular call/return sequence. This creates a mismatch between the apparent linear structure of execution within the coroutine and the externally observable re-entrant control flow introduced by await-suspend (https://godbolt.org/z/fez954Tsz).
This example also demonstrates the blurring of the definition of suspend point. In each await-expression of the example, the actual resumption of the coroutine occurs before the completion of the evaluation of the await-suspend and the formal establishment of the suspend point. As a result, suspend points may "collapse" or be established after control has actually been transferred (https://godbolt.org/z/csq6KcbGz).
All this leads to a loss of intuitive semantics.
The situation becomes more serious if exceptions are used. Consider example:
struct ResumeAndThrowAwaiter {
auto await_ready() { return false; }
auto await_suspend(std::coroutine_handle<> h) {
std::cout << 1;
h.resume(); // #3
std::cout << 3;
throw 0; // #4
}
auto await_resume() {}
};
struct Coro {
struct promise_type { /* ... */ };
};
Coro coro() {
try {
co_await ResumeAndThrowAwaiter{}; // #1
} catch (...) {
std::cout << '?';
}
std::cout << 2;
try {
co_await std::suspend_always{}; // #2
} catch (...) {
std::cout << 4;
}
std::cout << 5;
}
I see the execution as follows. The coroutine is suspended at #1, then resumed at #3, after which it is suspended at #2. Next, control is transferred back to #3, after which the evaluation of the await-suspend expression exits via an exception thrown at #4. This exception is rethrown from #2 and should be caught by the try block associated with this point. So I expect the output to be 12345.
The problem with this expectation is that [expr.await] operates on await-ready, await-suspend and await-resume, and also specifies the evaluation and exception handling for a single await-expression. According to [expr.await]/5.2, on the one hand, the coroutine resumes normally at #1, which implies that the corresponding await-resume should be evaluated, but on the other hand, the await-suspend of that same await-expression exits via an exception, which implies that its await-resume should not be evaluated. These conclusions are in conflict when interpreted within a single await-expression.
In this example, however, the execution interleaves the evaluations of two distinct await-expressions, which is not explicitly covered by [expr.await]. Intuitively, one would expect the exception to be thrown from #2, so that the coroutine resumes without evaluating the await-resume corresponding to #2 - effectively behaving as if the exception were propagated across suspension; this behavior is referred to as "exception teleportation". To describe such behavior precisely, it would be necessary to describe the semantics in terms of await-suspend and await-resume of different await-expressions.
If we look at major compilers behavior, they all print 123?2 - a kind of "time travel".
exception teleportation allows an exception that occurred in the context of one await-expression to appear in the context of another await-expression. This leads to strange results. Consider an example:
struct ResumeAndThrowAwaiter {
auto await_ready() { return false; }
auto await_suspend(std::coroutine_handle<> h) {
h.resume();
throw 0;
}
auto await_resume() {}
};
struct Coro {
struct promise_type {
std::suspend_always final_suspend() noexcept { return {}; }
// ...
};
};
Coro coro() {
co_await ResumeAndThrowAwaiter{};
}
Consider exception teleportation to the final await expression. This is acceptable because the standard does not unify the ways of resuming coroutines, and only prohibits explicit resumption by resume() call from the final suspend point. However, [dcl.fct.def.coroutine]/16 suggests that the intention is to prevent exceptions from being thrown in this context, but exception teleportation bypasses this. It is also unclear what happens when the coroutine is completed by exception.
Moreover, exception teleportation allows exceptions to be thrown in the context of expressions that are nevertheless non-throwing, which appears to be a gap in the specification.
exception teleportation may also be considered in the context of a different type of final suspend point. Example:
struct Awaiter {
auto await_ready() { return false; }
auto await_suspend(std::coroutine_handle<> h) {
h.resume(); // #1
return false;
}
auto await_resume() { throw 0; }
};
struct Coro {
struct promise_type {
void unhandled_exception() { throw 0; }
// ...
};
};
Coro coro() {
co_await Awaiter{};
}
Here according to [dcl.fct.def.coroutine]/15, the exception thrown from the unhandled_exception() call should propagate to #1, after which the await-suspend evaluation exits via that exception, and the coroutine resumes from its final suspend point with the rethrowing of this exception. Unlike the case of the final await expression, the standard does not indicate any intent regarding exceptions in this context.
The standard does not address resumption of the coroutine at its final suspend point in this scenario at all. It is unclear how this final suspend point relates to the replacement body, in particular where control resumes, and consequently where an exception would be thrown and where it would be caught. If an exception is caught at #1 and the coroutine resumes normally, it is unclear whether execution proceeds to the final-suspend label. Moreover, the final suspend point considered in the example is not associated with any await-expression, and therefore there is no corresponding await-resume. As a result, the semantics of such a resumption, whether with or without an exception, is unclear.
Although an await-expression is specified as a single evaluation, the semantics described for it involve multiple distinct effects that may be separated by suspension and resumption. As a result, it is not explicit how the constituent operations relate to the usual expression model. In particular, it is unclear whether await-ready, await-suspend, and await-resume are to be regarded as parts of a single full-expression, or as belonging to several distinct full-expressions.
If they are treated as separate full-expressions, then it is not specified:
- which of them form
full-expressions,
- which are subexpressions of other,
- and what the surrounding expressions are.
Intuitively, await-ready and await-resume are evaluated as part of the coroutine’s execution, whereas await-suspend is evaluated after the coroutine is considered suspended; however, this boundary is not explicitly specified.
This is of particular importance when considering object lifetimes. The expressions within an await-expression (await-ready, await-suspend, and await-resume) involve ordinary function calls. Their parameters may:
- have default arguments,
- create temporary objects (via temporary materialization),
- have non-trivial constructors and destructors,
- throw exceptions.
Such behavior is allowed by [expr.await]/3.
It is worth noting that [expr.await]/3.5 describes the handle in terms of an object, rather than specifying the value category of the expression used to pass it to await-suspend, leaving the value category unclear (https://godbolt.org/z/h91nPTW7f).
struct T {
~T() noexcept (false) { throw 0; }
};
struct H {
std::coroutine_handle<> h_;
H(auto h) : h_(h) {}
~H() noexcept (false) { throw 0; }
};
struct Awaiter {
auto await_ready(T = {}) { return false; }
auto await_suspend(H h) { h.h_.resume(); }
auto await_resume(T && = {}) {}
};
In this case, it is unclear:
This leads to the following problem: if the destructor of such an object throws an exception, it is not specified at what point in the semantics of the await-expression this exception is observed.
The await-suspend case is particularly problematic. The standard explicitly specifies the exception handling that occurs during the evaluation of await-suspend, but does not specify whether this behavior also applies to exceptions arising from the destruction of objects associated with that call (such as parameters or temporary objects). Thus, it is unclear whether such exceptions fall under [expr.await]/5.1.sentence-3, or are observed in a different context (https://godbolt.org/z/EsW8KrqqP).
An additional complication arises from the fact that the standard permits resumption of the coroutine from within await-suspend. As a result, destruction of parameters may occur after the evaluation of await-suspend, at a point where execution has already passed through the corresponding full-expression within the coroutine. Conversely, temporary objects may be destroyed as part of the full-expression of the await-expression, before the evaluation of await-suspend completes (https://godbolt.org/z/bYfb9bTWT).
Finally, the situation is further complicated when the await-expression appears in a context that extends the lifetime of temporary objects (for example, in the for-range-initializer of a range-based for statement). In such cases, destruction of temporary objects may occur well after completion of the corresponding calls, making it difficult to determine when exceptions are observed and how they are handled (https://godbolt.org/z/YPWEs4zY5).
Moreover, it is unclear whether lifetime extension (for example, in the for-range-initializer of a range-based for statement) applies to temporary objects created during initialization of the parameters of await-suspend. This is particularly unclear because await-suspend is evaluated after the coroutine is considered suspended, and may therefore be observed outside the immediate execution context of the await-expression.
This becomes particularly problematic when combined with resumption from within await-suspend. If lifetime extension applies at the point where the await-expression appears, covering all expressions involved in its evaluation, then resuming the coroutine from within await-suspend may cause that lifetime extension to end before control returns to the await-suspend call. As a result, objects whose lifetimes were extended may already be destroyed, leaving the await-suspend invocation with effectively dangling parameters.
The situation becomes significantly more complex when a full-expression contains multiple await-expressions, whether sequenced or not, as well as in cases where await-suspend performs multiple calls to std::coroutine_handle<>::resume() on the current coroutine.
All of the issues described above can arise even if await-suspend returns the handle h of the current coroutine, resulting in symmetric transfer. In this case, per [expr.await]/5.1.1.sentence-1, await-suspend effectively performs a resumption of the coroutine before the evaluation of the surrounding full-expression has completed, so that neither the evaluation of the await-suspend expression nor that of the enclosing await-expression is complete at the point of resumption.
An additional source of ambiguity arises from the result of await-ready. According to [expr.await]/3.6, the await-ready expression is only required to be contextually convertible to bool, and therefore may have a non-trivial type with side effects, including those occurring during destruction.
The standard specifies the behavior of await-expression in terms of conditional evaluation (depending on the result of await-ready), rather than as a sequence of operations. As a result, it is not specified in which context the result of await-ready is used, nor when its lifetime ends relative to the suspension of the coroutine.
In particular, if the result of await-ready is false, it is unclear at what point the coroutine is considered suspended with respect to the lifetime of that result, and when the destruction of the temporary object occurs. It is also unclear whether such destruction may occur before the evaluation of await-suspend.
Since the destruction of the result may have observable side effects, it is not specified whether the destructor of the result of await-ready may resume the coroutine, including prior to the evaluation of await-suspend (https://godbolt.org/z/nh4Yde9Y4).
A similar issue arises during the evaluation of the arguments of await-suspend. Since function arguments are evaluated prior to the invocation, and their evaluation may have arbitrary side effects, it is possible for a coroutine to be resumed during the evaluation of an argument to await-suspend. This resumption may occur before the invocation of await-suspend itself, which is not explicitly addressed by the standard (https://godbolt.org/z/TbWeM9Ycv).
Thus, resumption may occur at any point where side effects are permitted during evaluation of the await-expression.
A distinct problem arises when evaluating the initial await expression. Consider an example:
struct T {
~T() noexcept (false) { throw 0; }
};
struct Awaiter {
auto await_ready() { return false; }
auto await_suspend(std::coroutine_handle<> h, T && = {}) { h.resume(); }
auto await_resume() {}
};
struct Coro {
struct promise_type {
Awaiter initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
// ...
};
};
Coro coro() {
co_return;
}
The initial await expression determines the transition to the execution of the coroutine function body and is evaluated before it. Resuming the coroutine from within its await-suspend allows the execution of the function body to begin (and even complete) before the evaluation of the initial await expression is completed, blurring the boundary between coroutine start and execution. This also allows objects associated with the initial await expression to outlive the execution of the function body, although they are created before entering it. As a result, the standard does not clearly specify the semantics of destruction of such objects (logically related to the initial await expression), in particular if their destructors throw exceptions, just as if await-suspend would be used as await-suspend.resume().
Consider yet another distinct problem pertaining to the destroy() call.
The coroutine is considered suspended before the start of the evaluation of the await-suspend expression, while the evaluation of the await-expression is still in progress. At this point, the following objects may exist:
- block variables within the coroutine,
- objects involved in the evaluation of the
await-suspend expression (and, more generally, the await-expression), such as parameters and temporary objects associated with that evaluation.
The standard does not specify how these objects are maintained between suspension and resumption of the coroutine (in terms of storage).
While the lifetime of such objects has not ended, the coroutine can be destroyed by calling destroy(). In this case, control in the coroutine is considered to be transferred out of the function ([dcl.fct.def.coroutine]/12.sentence-2), i.e. beyond the end of the replacement body. [stmt.dcl] specifies the destruction of block variables, but does not cover objects related to the await-expression evaluation. The context and semantics of the destruction of the latter are unclear. The standard does not specify how the lifetime of objects associated with the uncompleted evaluation of the await-expression interacts with the destruction of the coroutine state when destroy() is called.
With respect to block variables, as I read the standard, if a destructor exits via an exception, control is transferred to the nearest matching handler, and the coroutine resumes execution instead of destroying. Thus, a call to destroy() effectively becomes a resume() call, which does not correspond to its specified effect (https://godbolt.org/z/fd1rMYMf7).
It is unclear how this interacts with other parts of the coroutine definition:
- should the handler in the
replacement body be triggered,
- what happens if the coroutine resumes in this way before
initial-await-resume-called is set to true,
- and if the call to
unhandled_exception() exits via an exception, should that exception be rethrown from the call to destroy() (https://godbolt.org/z/voP6erMqK).
Further complications arise if destroy() is invoked from within the await-suspend. It is unclear whether the coroutine can be correctly destroyed in this situation (https://godbolt.org/z/K8KjG4ET6).
The issues described above are not independent corner cases. They all stem from a single underlying inconsistency: an await-expression is specified as a single expression with well-defined evaluation semantics, while its execution is allowed to be split across suspension and resumption, including reentrant resumption before completion. The await-expression is not indivisible with respect to either evaluation or lifetime.
Although the standard describes the behavior of an await-expression in terms of a sequence of logically ordered steps, these steps are not defined as a complete operational model that accounts for all relevant aspects, including the lifetimes of parameters and results, lifetime extension, side effects and exceptions, reentrancy, destruction, and the boundaries of execution at coroutine entry and exit.
This mismatch manifests at multiple levels:
- within a single
await-expression,
- between multiple
await-expressions,
- at coroutine start (
initial suspend point),
- and at coroutine end (
final suspend point and exception handling).
The standard does not provide a unified or consistent model that explains how these behaviors interact.
Full name of submitter: Artyom Kolpakov
Consider the semantics of
await-expressionin [expr.await]On the one hand, the standard in [expr.await]/3, [expr.await]/4, [expr.await]/5 and [intro.execution]/12 treats an
await-expressionas a single expression. Its subexpressions (await-ready,await-suspend,await-resume) are part of a single evaluation, and the result of theawait-expressionis the result of theawait-resumeexpression. Thus, theawait-expressionfollows the usual rules of expression evaluation, including sequencing, lifetime of temporaries, and exception propagation.On the other hand, [expr.await]/5 specifies the semantics of an
await-expressionin terms of a sequence of conditionally evaluated subexpressions and associated effects, including a potential suspension and subsequent resumption of the coroutine. This introduces an observable discontinuity in execution, even though theawait-expressionremains a single evaluation in the abstract machine.As I read [expr.await]/5, the evaluation proceeds as follows.
First, the
await-readyexpression is evaluated. If its evaluation (or the evaluation of preceding subexpressions of theawait-expression) exits via an exception, the coroutine is not suspended, and the exception is thrown in the coroutine context from theawait-expression. Otherwise, if the result ofawait-readyistrue, the coroutine is not suspended, and theawait-resumeexpression is evaluated as the result of theawait-expression. Otherwise, the result ofawait-readyisfalse, and the coroutine is suspended (formally, considered suspended) in the middle of theawait-expressionevaluation, but control has not yet been transferred to the caller or resumer of the coroutine.Now
await-suspendis evaluated. If its evaluation exits via an exception, the exception is caught, the coroutine is resumed, and the exception is rethrown in the coroutine context from theawait-expression, without the evaluation of theawait-resumeexpression. Otherwise,await-suspendcompletes normally, control is transferred to the caller or resumer of the coroutine, regardless of the result ofawait-suspend, and the point of its suspension determinessuspend point.If the
await-suspendreturnsfalseor handlehthat refers to the coroutine, the coroutine is immediately resumed implicitly or viah.resume(), respectively. Otherwise, ifawait-suspendreturns the handlehthat refers to other coroutine, that coroutine resumed viah.resume(). Otherwise, control is "finally" transferred to the caller or resumer of the coroutine (exiting a coroutine call or a resume call).And when the coroutine is resumed, the
await-resumeexpression is evaluated.It is possible to distinguish several stages in the described semantics of a single
await-expression:await-expressioncontinues (and the evaluation of theawait-suspendbegins);await-suspend, control may be transferred to the caller or resumer, and asuspend pointis established.Thus, the evaluation of the
await-expressioncan be suspended and resumed later. At the same time, the standard permits resuming the coroutine via handle that refers to it during the evaluation of theawait-suspend, that is, between stages and before thesuspend pointis established.The interaction between the fact that the
await-expressionis a singleevaluation, the staged structure of its specified semantics, and the possibility of resumption duringawait-suspendis not fully specified. This leads to a number of subtle issues.Consider the following awaiter:
Although simple, it already reveals a number of problems. Since the coroutine is resumed before the
await-suspendevaluation completes, there is no control transfer to caller or resumer of the coroutine between its suspension and resumption, which is inconsistent with [expr.await]/1.sentence-2. This also means that there is no establishedsuspend pointwhen resuming. The control transfer and the establishment of thesuspend pointwill occur later, in fact, when it is no longer necessary. And consistency is lost with theinitial suspend pointand thefinal suspend pointA simple example:
It demonstrates receiving the result of the
await-expressionbefore its evaluation completes. When theawait-suspendresumes the coroutine before its completion, theawait-resumeevaluation already begins, although the evaluation of theawait-expressionwill not end with it. Moreover, in this way the evaluation of theawait-expressionextends beyond itsfull-expression.Another example:
(co_await Awaiter{})[ptr];It shows a violation of the ordering. According to [expr.sub]/1, the
await-expressionshould be fully evaluated before evaluation of theptr. However, due to the resumption from within theawait-suspend, theawait-expressionhas not yet completed evaluation. And again, the result of theawait-expressionis observed before it evaluation completes.Yet another example:
It results in overlapping evaluation of consecutive
await-expressions. Each subsequentawait-expressionstarts to be evaluated before the previous one is evaluated. This leads to the existence of severalawait-expressionsbeing evaluated at the same time, which destroys the sequential execution model ([intro.execution]).More fundamentally, explicit resumption of the coroutine from within the
await-suspenddoes not follow the usual linear call/return execution model and introduces re-entrant execution of the coroutine. Each such resumption may result in additional nested activations associated with the sameawait-expression, occurring after the coroutine is considered suspended. At the same time, from within the coroutine itself, execution continues to appear as a regular call/return sequence. This creates a mismatch between the apparent linear structure of execution within the coroutine and the externally observable re-entrant control flow introduced byawait-suspend(https://godbolt.org/z/fez954Tsz).This example also demonstrates the blurring of the definition of
suspend point. In eachawait-expressionof the example, the actual resumption of the coroutine occurs before the completion of the evaluation of theawait-suspendand the formal establishment of thesuspend point. As a result,suspend points may "collapse" or be established after control has actually been transferred (https://godbolt.org/z/csq6KcbGz).All this leads to a loss of intuitive semantics.
The situation becomes more serious if exceptions are used. Consider example:
I see the execution as follows. The coroutine is suspended at
#1, then resumed at#3, after which it is suspended at#2. Next, control is transferred back to#3, after which the evaluation of theawait-suspendexpression exits via an exception thrown at#4. This exception is rethrown from#2and should be caught by the try block associated with this point. So I expect the output to be12345.The problem with this expectation is that [expr.await] operates on
await-ready,await-suspendandawait-resume, and also specifies the evaluation and exception handling for a singleawait-expression. According to [expr.await]/5.2, on the one hand, the coroutine resumes normally at#1, which implies that the correspondingawait-resumeshould be evaluated, but on the other hand, theawait-suspendof that sameawait-expressionexits via an exception, which implies that itsawait-resumeshould not be evaluated. These conclusions are in conflict when interpreted within a singleawait-expression.In this example, however, the execution interleaves the evaluations of two distinct
await-expressions, which is not explicitly covered by [expr.await]. Intuitively, one would expect the exception to be thrown from#2, so that the coroutine resumes without evaluating theawait-resumecorresponding to#2- effectively behaving as if the exception were propagated across suspension; this behavior is referred to as "exception teleportation". To describe such behavior precisely, it would be necessary to describe the semantics in terms ofawait-suspendandawait-resumeof differentawait-expressions.If we look at major compilers behavior, they all print
123?2- a kind of "time travel".exception teleportationallows an exception that occurred in the context of oneawait-expressionto appear in the context of anotherawait-expression. This leads to strange results. Consider an example:Consider
exception teleportationto thefinal await expression. This is acceptable because the standard does not unify the ways of resuming coroutines, and only prohibits explicit resumption byresume()call from thefinal suspend point. However, [dcl.fct.def.coroutine]/16 suggests that the intention is to prevent exceptions from being thrown in this context, butexception teleportationbypasses this. It is also unclear what happens when the coroutine is completed by exception.Moreover,
exception teleportationallows exceptions to be thrown in the context of expressions that are nevertheless non-throwing, which appears to be a gap in the specification.exception teleportationmay also be considered in the context of a different type offinal suspend point. Example:Here according to [dcl.fct.def.coroutine]/15, the exception thrown from the
unhandled_exception()call should propagate to#1, after which theawait-suspendevaluation exits via that exception, and the coroutine resumes from itsfinal suspend pointwith the rethrowing of this exception. Unlike the case of thefinal await expression, the standard does not indicate any intent regarding exceptions in this context.The standard does not address resumption of the coroutine at its
final suspend pointin this scenario at all. It is unclear how thisfinal suspend pointrelates to thereplacement body, in particular where control resumes, and consequently where an exception would be thrown and where it would be caught. If an exception is caught at#1and the coroutine resumes normally, it is unclear whether execution proceeds to thefinal-suspendlabel. Moreover, thefinal suspend pointconsidered in the example is not associated with anyawait-expression, and therefore there is no correspondingawait-resume. As a result, the semantics of such a resumption, whether with or without an exception, is unclear.Although an
await-expressionis specified as a singleevaluation, the semantics described for it involve multiple distinct effects that may be separated by suspension and resumption. As a result, it is not explicit how the constituent operations relate to the usual expression model. In particular, it is unclear whetherawait-ready,await-suspend, andawait-resumeare to be regarded as parts of a singlefull-expression, or as belonging to several distinctfull-expressions.If they are treated as separate
full-expressions, then it is not specified:full-expressions,Intuitively,
await-readyandawait-resumeare evaluated as part of the coroutine’s execution, whereasawait-suspendis evaluated after the coroutine is considered suspended; however, this boundary is not explicitly specified.This is of particular importance when considering object lifetimes. The expressions within an
await-expression(await-ready,await-suspend, andawait-resume) involve ordinary function calls. Their parameters may:Such behavior is allowed by [expr.await]/3.
It is worth noting that [expr.await]/3.5 describes the handle in terms of an object, rather than specifying the value category of the expression used to pass it to
await-suspend, leaving the value category unclear (https://godbolt.org/z/h91nPTW7f).In this case, it is unclear:
full-expression,This leads to the following problem: if the destructor of such an object throws an exception, it is not specified at what point in the semantics of the
await-expressionthis exception is observed.The
await-suspendcase is particularly problematic. The standard explicitly specifies the exception handling that occurs during the evaluation ofawait-suspend, but does not specify whether this behavior also applies to exceptions arising from the destruction of objects associated with that call (such as parameters or temporary objects). Thus, it is unclear whether such exceptions fall under [expr.await]/5.1.sentence-3, or are observed in a different context (https://godbolt.org/z/EsW8KrqqP).An additional complication arises from the fact that the standard permits resumption of the coroutine from within
await-suspend. As a result, destruction of parameters may occur after the evaluation ofawait-suspend, at a point where execution has already passed through the correspondingfull-expressionwithin the coroutine. Conversely, temporary objects may be destroyed as part of thefull-expressionof theawait-expression, before the evaluation ofawait-suspendcompletes (https://godbolt.org/z/bYfb9bTWT).Finally, the situation is further complicated when the
await-expressionappears in a context that extends the lifetime of temporary objects (for example, in thefor-range-initializerof a range-based for statement). In such cases, destruction of temporary objects may occur well after completion of the corresponding calls, making it difficult to determine when exceptions are observed and how they are handled (https://godbolt.org/z/YPWEs4zY5).Moreover, it is unclear whether lifetime extension (for example, in the
for-range-initializerof a range-based for statement) applies to temporary objects created during initialization of the parameters ofawait-suspend. This is particularly unclear becauseawait-suspendis evaluated after the coroutine is considered suspended, and may therefore be observed outside the immediate execution context of theawait-expression.This becomes particularly problematic when combined with resumption from within
await-suspend. If lifetime extension applies at the point where theawait-expressionappears, covering all expressions involved in its evaluation, then resuming the coroutine from withinawait-suspendmay cause that lifetime extension to end before control returns to theawait-suspendcall. As a result, objects whose lifetimes were extended may already be destroyed, leaving theawait-suspendinvocation with effectively dangling parameters.The situation becomes significantly more complex when a
full-expressioncontains multipleawait-expressions, whether sequenced or not, as well as in cases whereawait-suspendperforms multiple calls tostd::coroutine_handle<>::resume()on the current coroutine.All of the issues described above can arise even if
await-suspendreturns the handlehof the current coroutine, resulting in symmetric transfer. In this case, per [expr.await]/5.1.1.sentence-1,await-suspendeffectively performs a resumption of the coroutine before the evaluation of the surroundingfull-expressionhas completed, so that neither the evaluation of theawait-suspendexpression nor that of the enclosingawait-expressionis complete at the point of resumption.An additional source of ambiguity arises from the result of
await-ready. According to [expr.await]/3.6, theawait-readyexpression is only required to be contextually convertible tobool, and therefore may have a non-trivial type with side effects, including those occurring during destruction.The standard specifies the behavior of
await-expressionin terms of conditional evaluation (depending on the result ofawait-ready), rather than as a sequence of operations. As a result, it is not specified in which context the result ofawait-readyis used, nor when its lifetime ends relative to the suspension of the coroutine.In particular, if the result of
await-readyisfalse, it is unclear at what point the coroutine is considered suspended with respect to the lifetime of that result, and when the destruction of the temporary object occurs. It is also unclear whether such destruction may occur before the evaluation ofawait-suspend.Since the destruction of the result may have observable side effects, it is not specified whether the destructor of the result of
await-readymay resume the coroutine, including prior to the evaluation ofawait-suspend(https://godbolt.org/z/nh4Yde9Y4).A similar issue arises during the evaluation of the arguments of
await-suspend. Since function arguments are evaluated prior to the invocation, and their evaluation may have arbitrary side effects, it is possible for a coroutine to be resumed during the evaluation of an argument toawait-suspend. This resumption may occur before the invocation ofawait-suspenditself, which is not explicitly addressed by the standard (https://godbolt.org/z/TbWeM9Ycv).Thus, resumption may occur at any point where side effects are permitted during evaluation of the await-expression.
A distinct problem arises when evaluating the
initial await expression. Consider an example:The
initial await expressiondetermines the transition to the execution of the coroutinefunction bodyand is evaluated before it. Resuming the coroutine from within itsawait-suspendallows the execution of thefunction bodyto begin (and even complete) before the evaluation of theinitial await expressionis completed, blurring the boundary between coroutine start and execution. This also allows objects associated with theinitial await expressionto outlive the execution of thefunction body, although they are created before entering it. As a result, the standard does not clearly specify the semantics of destruction of such objects (logically related to theinitial await expression), in particular if their destructors throw exceptions, just as ifawait-suspendwould be used asawait-suspend.resume().Consider yet another distinct problem pertaining to the
destroy()call.The coroutine is considered suspended before the start of the evaluation of the
await-suspendexpression, while the evaluation of theawait-expressionis still in progress. At this point, the following objects may exist:await-suspendexpression (and, more generally, theawait-expression), such as parameters and temporary objects associated with that evaluation.The standard does not specify how these objects are maintained between suspension and resumption of the coroutine (in terms of storage).
While the lifetime of such objects has not ended, the coroutine can be destroyed by calling
destroy(). In this case, control in the coroutine is considered to be transferred out of the function ([dcl.fct.def.coroutine]/12.sentence-2), i.e. beyond the end of thereplacement body. [stmt.dcl] specifies the destruction of block variables, but does not cover objects related to theawait-expressionevaluation. The context and semantics of the destruction of the latter are unclear. The standard does not specify how the lifetime of objects associated with the uncompleted evaluation of theawait-expressioninteracts with the destruction of the coroutine state whendestroy()is called.With respect to block variables, as I read the standard, if a destructor exits via an exception, control is transferred to the nearest matching handler, and the coroutine resumes execution instead of destroying. Thus, a call to
destroy()effectively becomes aresume()call, which does not correspond to its specified effect (https://godbolt.org/z/fd1rMYMf7).It is unclear how this interacts with other parts of the coroutine definition:
replacement bodybe triggered,initial-await-resume-calledis set totrue,unhandled_exception()exits via an exception, should that exception be rethrown from the call todestroy()(https://godbolt.org/z/voP6erMqK).Further complications arise if
destroy()is invoked from within theawait-suspend. It is unclear whether the coroutine can be correctly destroyed in this situation (https://godbolt.org/z/K8KjG4ET6).The issues described above are not independent corner cases. They all stem from a single underlying inconsistency: an
await-expressionis specified as a single expression with well-defined evaluation semantics, while its execution is allowed to be split across suspension and resumption, including reentrant resumption before completion. Theawait-expressionis not indivisible with respect to either evaluation or lifetime.Although the standard describes the behavior of an
await-expressionin terms of a sequence of logically ordered steps, these steps are not defined as a complete operational model that accounts for all relevant aspects, including the lifetimes of parameters and results, lifetime extension, side effects and exceptions, reentrancy, destruction, and the boundaries of execution at coroutine entry and exit.This mismatch manifests at multiple levels:
await-expression,await-expressions,initial suspend point),final suspend pointand exception handling).The standard does not provide a unified or consistent model that explains how these behaviors interact.