C++20协程存取move-only数据时await_resume返回左值致段错误的解决方法
段错误根因
- 核心触发点:
await_ready()固定返回true,完全跳过await_suspend()的执行。根据C++协程规范,当await_ready()返回true时,协程不会挂起,也不会传递当前协程句柄给awaiter,导致awaitalbe实例的handle成员是默认构造的空句柄(等价于nullptr)。后续在await_resume()中访问handle.promise()就是空指针解引用,直接触发段错误。 - 执行顺序错配:
promise_type的initial_suspend()返回suspend_never,意味着协程创建后会立刻从头执行。在main中调用f()创建协程时,协程会一路跑到第一个co_await表达式位置,此时main还没执行set_data()给promise填充数据,就算句柄初始化正常,move出来的unique_ptr也是空的,后续解引用同样会触发段错误。
修正方案
按照问题点逐一调整:
- 修正awaitable逻辑,消除空句柄问题
强制走await_suspend流程拿到合法协程句柄,同时避免不必要的实际挂起:- 将
await_ready()的返回值改为false,确保一定会调用await_suspend,完成句柄的赋值 - 将
await_suspend()的返回类型从void改为bool,存入句柄后返回false,通知协程不要真正挂起,直接恢复执行 - 将
await_resume()的返回类型从右值引用改为值类型,直接move出仅移动数据,避免悬空引用风险
调整后的awaitable代码:
struct awaitalbe { bool await_ready() { return false; } bool await_suspend(std::coroutine_handle<task::promise_type> handle) { this->handle = handle; return false; // 不实际挂起协程 } task::promise_type::data_type await_resume() { return std::move(handle.promise().data); } std::coroutine_handle<task::promise_type> handle; }; - 将
- 调整initial_suspend,保证数据注入窗口
将promise_type中的initial_suspend()从suspend_never改为suspend_always,让协程创建完成后直接挂在起点,不会提前向下执行,给外部留足空间调用set_data()注入数据:std::suspend_always initial_suspend() const noexcept { return {}; } - 补充task的析构逻辑
由于final_suspend()返回suspend_always,协程执行完成后会挂在终点,不会自动释放协程帧内存。需要在task的析构函数中判断句柄有效性,主动销毁协程,避免内存泄漏:~task() { if (handle) { handle.destroy(); } }
调整后的执行流程
- main中调用
f()创建协程,构造promise并生成task对象 - 协程因
initial_suspend的设置直接挂起在起点,不向下执行 - main拿到task实例,通过
set_data()将构造好的仅移动数据move进协程的promise中 - 调用
task1.resume(),协程从起点恢复执行,走到co_await awaitalbe{}位置 - 触发
await_ready返回false,进入await_suspend流程完成句柄赋值,返回false不实际挂起 - 直接调用
await_resume将promise中的数据move出来,赋值给协程内的p2 - 协程继续执行,正常输出数据值,最终挂在
final_suspend位置 - task对象离开作用域时,自动调用
handle.destroy()释放协程帧内存
内容的提问来源于stack exchange,提问作者joey zeng
相关产品推荐
相关产品推荐

