协程参数的生命周期是多久?

5
#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();
}

输出结果为:output

1  constructor0
2 copy constructor0
3 move constructor0
await_suspend
2 destroy0
caller
await_resume
FinalSuspend await_suspend
3 destroy0
1 destroy0

使用上述代码,我们可以观察对象的创建或销毁。第一次打印发生在#1处,它构造了对象a。第二次打印发生在协程参数的初始化时,在#2处。第三个打印是在协程参数副本的初始化时发生的,其受以下规则控制:
[dcl.fct.def.coroutine#13] 引用如下:

调用协程后,在初始化其参数([expr.call])之后,为每个协程参数创建一个副本。对于类型为cv T的参数,该副本是具有自动存储期的cv T类型变量,该变量从引用参数的T类型的xvalue进行直接初始化。

这三个对象都有其独特的数字,方便观察相关对象的生命周期。根据第五个打印输出,析构函数被调用的是名为p协程参数。然而,根据[expr.await#5.1]
引用如下:

否则,控制流返回到当前协程调用者或恢复器([dcl.fct.def.coroutine]),不会退出任何作用域([stmt.jump])。

这意味着挂起协程并将控制传输给调用者时,协程参数的作用域并未被认为已经退出。因此,参数的生命周期不应该结束。那么为什么在第一次转移到协程的调用者后调用参数的析构函数?这是编译器中的漏洞吗?
1个回答

2
一个参数的生命周期不属于函数范围,而是调用者的范围的一部分:

每个参数的初始化和销毁都发生在调用函数的上下文中。

这就是[dcl.fct.def.coroutine#13]存在的全部原因。为了使协程保留其参数,它必须拥有它们。这意味着它必须将它们从参数复制/移动到本地自动存储中。

@xmh0511:挂起协程在“调用函数”的角度来看,相当于从函数调用中返回。我的观点是,参数的生命周期不受被调用函数的控制。它们不是其“作用域”的一部分,因此不会被协程的挂起所保留。 - Nicol Bolas
参数的作用域是函数参数作用域,它包含了函数复合语句引入的块作用域。因此,参数在该作用域内。当创建对象的块退出时,对于具有自动存储期限制的构造对象,析构函数会被隐式调用([basic.stc.auto]);在每种情况下,调用的上下文是对象构造的上下文。所以,正如你所说,它在调用者的上下文中执行。如果实现选择... - xmh0511
当定义参数的函数返回时,该参数的生命周期也随之结束。挂起确实会按照控制流返回到当前协程调用者或恢复者返回给调用者。但是,如果它选择后者,我不知道如何解释它(我的意思是,挂起协程并返回是否算作进入封闭完整表达式的结尾?)。 - xmh0511
@xmh0511:这里的“未指定”是什么意思?“协程通过co_return语句或在挂起时(7.6.2.3)返回给其调用者或恢复器(9.5.4)。”与“return”语句的规范一样明确:“函数通过return语句返回给其调用者。” - Nicol Bolas
1
@LanguageLawyer 在每种情况下,调用的上下文都是对象构造的上下文[class.dtor#15](https://eel.is/c++draft/class.dtor#15)提到了这一点。简单来说,由于调用者构造了对象,因此调用者负责销毁该对象。 - xmh0511
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接