Reputation: 2254
#include <iostream>
#include <coroutine>
class eager {
public:
struct promise_type {
promise_type() { std::cout << "promise_type ctor" << std::endl; }
~promise_type() { std::cout << "~promise_type dtor" << std::endl; }
struct return_object {
return_object() { std::cout << "return_object ctor" << std::endl; }
~return_object() { std::cout << "~return_object dtor" << std::endl; }
operator eager() { return {}; }
};
auto get_return_object() noexcept { return return_object{}; }
constexpr auto initial_suspend() const noexcept { return std::suspend_never{}; }
constexpr auto final_suspend() const noexcept { return std::suspend_never{}; }
constexpr auto return_void() const noexcept {}
auto unhandled_exception() -> void { throw; }
};
};
auto coroutine() -> eager {
co_return;
}
auto main() -> int
{
coroutine();
return 0;
}
You can see the result for MSVC, clang and GCC here: https://godbolt.org/z/Yan9s9TPE
According to lots of articles about coroutine, the coroutine()
will be transformed into...
auto coroutine() -> eager {
eager::promise_type promise;
auto res = promise.get_return_object();
// initial suspend
promise.return_void();
// final suspend
return res;
}
At a glance, since the promise object is constructed first, I thought it would be the last object to be destructed.
However, MSVC and GCC show reverse order:
// from MSVC/GCC
promise_type ctor
return_object ctor
~promise_type dtor
~return_object dtor
On the other hand, clang shows what I expected:
// from clang
promise_type ctor
return_object ctor
~return_object dtor
~promise_type dtor
Which one is right? Or, is the destruction order of promise object and return object just unspecified by standard?
Upvotes: 2
Views: 354
Reputation: 473946
According to lots of articles about coroutine
Then "lots of articles about coroutine[sic]" are incorrect.
The result object is not on the coroutine stack. It cannot be on the coroutine stack, because it's the result object for the initial call to the coroutine.
All that the C++ standard says about get_result_object
is this:
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 to get_return_object is sequenced before the call toinitial_suspend
and is invoked at most once.
It happens before initial_suspend
and it gets called once. That's all it has to say. Therefore, everything else about result objects from functions work as normal; in this case, it simply gets initialized before the function properly starts instead of when the function is about to return.
By C++'s normal rules, a function's result object is on the caller's stack, not the stack of the function being called. So when promise.get_result_object()
is evaluated, it is initializing storage provided by the caller.
main
discards the result of the expression coroutine()
. This means that it will manifest a temporary from the prvalue result object, and the type of this temporary will be eager
. The temporary will be destroyed, but only after control returns to main
.
Here's the tricky part: the return prvalue from get_result_object()
is not eager
. It's eager::promise::result_object
. Initializing the return value requires performing an implicit conversion from result_object
to eager
. This requires manifesting a temporary of type result_object
to perform that conversion on.
The standard rule for destroying temporaries is:
Temporary objects are destroyed as the last step in evaluating the full-expression ([intro.execution]) that (lexically) contains the point where they were created.
But... what is the "full-expression" here?
One would assume it would be the promise.get_result_object()
expression. But is that a "full-expression" by C++'s rules? These rules are rather esoteric and technical. One could argue that promise.get_result_object()
is being used to initialize an object and therefore it is de-facto an "init-declarator". But "init-declarator" is a piece of grammar, and promise.get_result_object()
is not stated by the text to be an "init-declarator".
An argument could be made that the only expression that is certainly a full expression is coroutine()
. And therefore, one could make the argument that any temporaries used to initialize the return value object should persist until control returns to the caller.
I would argue that the standard wording is under-specified and therefore both versions are equally legitimate until clarification is provided. Clang's version makes more sense (but not for the reasons you claim), but the others are at least arguable.
Upvotes: 3