I was tracking down a bug in gcc and encountered a situation for which I don't know what the acceptable behavior(s) are. Suppose we have, somewhere within a coroutine, a line such as:
Pair{.x{}, .y=(co_await std::suspend_always{}, 1)};
where Pair is some struct that can be aggregate initialized with members .x (of type X with a default constructor) and .y (with of type Y with a constructor from an int). These types may all have nontrivial destructors. What may this code do?
Section 9.4.1/6 of the C++20 standard states:
The initializations of the elements of the aggregate are evaluated in the element order. That is, all value computations and side effects associated with a given element are sequenced before those of any element that follows it in order.
which suggests to me that the only correct behavior for this line is as follows:
- Reserve space in the coroutine frame for an object of type
Pair. - Initialize the
.xmember via a call toX::X() - Evaluate the
co_awaitexpression (callingawait_readyandawait_suspend); suspend.
Then, if the coroutine is resumed, I expect:
- Call
await_resumeon the awaiter. - Initialize the
.ymember via a call toY::Y(int) - Call
Pair::~Pair()to destroy the temporary.
If the coroutine is instead destroyed, I expect:
- Call
X::~X()on the.xmember.
I'm fairly sure that this is correct behavior, though less sure whether it's the only allowable behavior - and my confusion is worsened by the fact that none of the major compilers output sensible code for this case. A full example on godbolt shows that both gcc and clang produce code where calls to destructors and constructors are not properly matched (which is the bug I was hunting down in gcc in the first place). MSVC gives an internal compiler error.
Is the suggested behavior correct? Is this the only correct behavior?