Reputation: 7369
#include <iostream>
#include <experimental/coroutine>
#include <string>
#include <thread>
struct InitialSuspend{
bool await_ready(){
return false;
}
bool await_suspend(std::experimental::coroutine_handle<> h){
return false;
}
void await_resume(){
}
};
struct FinalSuspend{
bool await_ready() noexcept{
return false;
}
void await_suspend(std::experimental::coroutine_handle<> h) noexcept{
std::cout<<"FinalSuspend await_suspend\n";
}
std::string await_resume() noexcept{
std::cout<< "await_resume for FinalSuspend\n";
return "await_resume for FinalSuspend\n";
}
};
struct Task{
struct promise_type;
using coroutine_type = std::experimental::coroutine_handle<promise_type>;
struct promise_type{
auto initial_suspend(){
return InitialSuspend{};
}
void unhandled_exception(){
std::cout<<"unhandled_exception\n";
std::terminate();
}
auto final_suspend() noexcept{
return FinalSuspend{};
}
// void return_value(std::string const& v){
// value_ = v;
// }
void return_void(){
}
auto get_return_object(){
return Task{coroutine_type::from_promise(*this)};
}
std::string value_;
};
coroutine_type handler_;
};
struct AwaitAble{
bool await_ready(){
return false;
}
void await_suspend(std::experimental::coroutine_handle<> h){
std::cout<<"await_suspend\n";
}
std::string await_resume(){
std::cout<<"await_resume\n";
return "abc";
}
};
struct Observe0{
Observe0(int v):id_(v){
std::cout<< id_ <<" constructor0\n";
}
~Observe0(){
std::cout<< id_ <<" destroy0\n";
}
Observe0(Observe0 const& v):id_(v.id_+1){
std::cout<< id_<<" copy constructor0\n";
}
Observe0(Observe0&& v):id_(v.id_+1){
std::cout<< id_<<" move constructor0\n";
}
int id_;
};
Task MyCoroutine(Observe0 p){
auto r1 = co_await AwaitAble{};
}
int main(){
Observe0 aa{1}; //#1
auto r = MyCoroutine(aa); //#2
std::cout<<"caller\n";
r.handler_.resume();
r.handler_.destroy();
std::cin.get();
}
The output is:
1 constructor0
2 copy constructor0
3 move constructor0
await_suspend
2 destroy0
caller
await_resume
FinalSuspend await_suspend
3 destroy0
1 destroy0
We can observe the creation or destruction of an object by using the above code. The first print occurs at #1
, which constructs the object a
. The second print occurs at the initialization of the coroutine parameter at #2
. The third print occurs at the initialization of the coroutine parameter copy, which is ruled by the following rules:
[dcl.fct.def.coroutine#13]
When a coroutine is invoked, after initializing its parameters ([expr.call]), a copy is created for each coroutine parameter. For a parameter of type cv T, the copy is a variable of type cv T with automatic storage duration that is direct-initialized from an xvalue of type T referring to the parameter.
These three objects all have their unique number which is convenient to observe the lifetime of the associated object. According to the fifth print, the destructor is called for the coroutine parameter whose name is p
. However, according to [expr.await#5.1]
Otherwise, control flow returns to the current coroutine caller or resumer ([dcl.fct.def.coroutine]) without exiting any scopes ([stmt.jump]).
It means that suspend the coroutine and transfer the control to the caller, the parameter scope of the coroutine is not considered exit. Hence, the lifetime of the parameter should not end. Why is the destructor of the parameter called after the first transferring to the caller of the coroutine? Should it be considered a bug in the compiler?
Upvotes: 5
Views: 668
Reputation: 474036
The lifetime of a parameter is not part of the function's scope; it's part of the caller's scope:
The initialization and destruction of each parameter occurs within the context of the calling function.
This is the entire reason why [dcl.fct.def.coroutine#13] exists. In order for a coroutine to preserve its parameters, it has to own them. Which means it must copy/move them from the parameters into local automatic storage.
Upvotes: 2