Order of calls in aggregate initialization over a co_await statement

Viewed 136

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:

  1. Reserve space in the coroutine frame for an object of type Pair.
  2. Initialize the .x member via a call to X::X()
  3. Evaluate the co_await expression (calling await_ready and await_suspend); suspend.

Then, if the coroutine is resumed, I expect:

  1. Call await_resume on the awaiter.
  2. Initialize the .y member via a call to Y::Y(int)
  3. Call Pair::~Pair() to destroy the temporary.

If the coroutine is instead destroyed, I expect:

  1. Call X::~X() on the .x member.

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?

0 Answers
Related