I've been writing a coroutine library, and I've run into a peculiar problem. In some cases, the construction of the coroutine result object was sequenced after the call to initial_suspend.
Question: is this sequencing a bug on the part of the compiler?
Background
In my particular case, this was causing a crash, as a generator promise executed under the assumption that it had no owner.
The relevant section of the C++20 standard is section 9.5.4.7, which states:
The expression
promise.get_return_object()is used to initialize the glvalue result or prvalue result object of a call to a coroutine. The call toget_return_objectis sequenced before the call toinitial_suspendand is invoked at most once.
When I read this portion of the standard, I originally interpreted it to mean that the initialization of the coroutine's result object is sequenced immediately after the call to promise.get_return_object(): when you initialize something, initialization occurs immediately after the computation of the arguments to the constructor, and not as a delayed effect.
Unfortunately, this is not the behavior I observed. The source of the crash is that the initialization of the result object was sequenced after the initial suspend, despite the call to promise.get_return_object() being sequenced before the initial suspend.
Observing this behavior in code (view live example here)
Let's write a very simple type that can be returned from a coroutine:
template <class Promise>
struct coroutine {
std::coroutine_handle<Promise> handle;
using promise_type = Promise;
coroutine(std::coroutine_handle<Promise> p) : handle(p) {
std::cout << " Running coroutine(std::coroutine_handle<Promise> p)\n";
}
~coroutine() {
if (handle) {
handle.destroy();
}
}
};
Because coroutine<Promise> can be constructed from a std::coroutine_handle<Promise>, the call to promise.get_return_object() can return either a std::coroutine_handle<Promise> which is used to construct the coroutine, or it can return a
coroutine<Promise> directly.
Let's write two different promise types, one for each option:
struct promise_base {
std::suspend_never initial_suspend() {
std::cout << " Running initial_suspend()\n";
return {};
}
std::suspend_always final_suspend() { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
struct promise_A : promise_base {
using handle = std::coroutine_handle<promise_A>;
handle get_return_object() {
std::cout << " Running get_return_object()\n";
return handle::from_promise(*this);
}
};
struct promise_B : promise_base {
using handle = std::coroutine_handle<promise_B>;
coroutine<promise_B> get_return_object() {
std::cout << " Running get_return_object()\n";
return {handle::from_promise(*this)};
}
};
We can then write two identical coroutines based on promise_A and promise_B:
coroutine<promise_A> run_A() {
std::cout << "Inside coroutine body\n";
co_return;
}
coroutine<promise_B> run_B() {
std::cout << "Inside coroutine body\n";
co_return;
}
In this code,
promise_Areturns a handle fromget_return_object, and this is used to construct a result object of typecoroutine<promise_A>.promise_Bdirectly returns acoroutine<promise_B>, which is returned as the result object.
In the case of promise_A, the construction of the coroutine<promise_A> result object is sequenced after the call to initial_suspend().
We can write the following test code to verify this:
void test_promise_A() {
std::cout << "------ Testing A ------\n";
run_A();
std::cout << "\n\n";
}
void test_promise_B() {
std::cout << "------ Testing B ------\n";
run_B();
std::cout << "\n\n";
}
int main() {
test_promise_A();
test_promise_B();
}
Under GCC 10.1, this produces the following output. Note the difference in sequencing between Testing A and Testing B.
------ Testing A ------
Running get_return_object()
Running initial_suspend()
Inside coroutine body
Running coroutine(std::coroutine_handle<Promise> p)
------ Testing B ------
Running get_return_object()
Running coroutine(std::coroutine_handle<Promise> p)
Running initial_suspend()
Inside coroutine body
To reiterate the question: Is the sequencing of Testing A a bug?