C++co_return协程返回值处理
co_return 不是“return”,它在替你悄悄打包快递
写协程时,第一次看到 co_return 42;,我下意识想:这不就是带个 co_ 前缀的 return 吗?结果调试半天,发现值没传出去,await_resume() 里拿到的是个空壳——原来,co_return 的真正工作,是调用 promise 对象的 return_value() 或 return_void(),再由它决定怎么把结果塞进协程状态机里。它不直接返回,它是在调度、委托、封装。
很多人卡在这一步:协程函数声明了返回 Task<int>,co_return 123; 写得挺顺,可一跑起来,Task 里存的值却是未定义的。问题往往不出在 Task 实现上,而出在 promise_type 没配对——co_return 的语义完全由 promise_type::return_value(T&&) 是否存在、是否正确实现来定义。
举个具体例子。假设你写了这样一个协程:
Task<int> get_answer() {
co_return 42;
}
编译器看到 co_return 42;,会立刻去找 Task<int>::promise_type 里有没有 return_value(int&&) 成员函数。如果有,就调用它;如果没有,但 42 是 void 类型,它才退而求其次找 return_void();如果两个都没有,编译直接报错:“no matching function for call to ‘return_value’”。
这里有个容易被忽略的细节:co_return expr; 传递给 return_value 的实参类型,取决于 expr 的值类别和 return_value 的形参签名。比如:
co_return 42;→ 传int&&(纯右值)co_return x;(x 是int变量)→ 传int&,除非你显式写co_return std::move(x);- 如果
promise_type::return_value只声明了void return_value(int&&),那co_return x;就会编译失败。
所以,别急着写 co_return,先盯住你的 promise_type。常见错误是只实现了 return_void()(比如协程不返回值),却在需要返回值的地方硬写 co_return val;——这不是语法错,是语义断层。
更实际的问题是:如何让 co_return 把值安全地“交到用户手上”?
关键在 promise_type 怎么存这个值,以及 Task 的 await_resume() 怎么取。典型做法是:在 promise 对象里加一个 std::optional<T> result_,return_value 负责移动构造进去;await_resume() 则 return std::move(result_).value();。但注意——await_resume() 必须在协程已结束(done() == true)后才被调用,否则 result_ 可能还没被赋值。
还有一种轻量级方案:不存值,而是把值直接塞进 Task 对象本身。比如让 Task<T> 的构造函数接收 promise_type*,并在内部通过 promise.get_return_object_on_allocation() 或类似机制,在分配时就把 T 的存储布局预留好。这种写法性能更好,但要求 promise_type 和 Task 紧密耦合,适合自研协程库,不适合快速原型。
顺便提一句:co_return;(无表达式)和 co_return expr; 是两条不同的路径。前者强制走 return_void(),后者走 return_value()。哪怕你的协程逻辑上“不返回有意义的值”,只要函数签名是 Task<int>,就必须提供 return_value(int&&)——哪怕只是 static_assert(false, "should not happen");,也比漏掉强。编译器不会帮你“猜意图”,它只认签名。
最后,一个真实踩坑场景:异常传播。如果 co_return expr; 中的 expr 构造过程抛异常(比如 std::string 移动构造时 OOM),这个异常会绕过 return_value(),直接进入协程的异常处理流程(触发 unhandled_exception())。这意味着——co_return 不是原子操作,它的“返回”可能被异常截断,return_value() 根本不会执行。所以,如果你在 return_value() 里做了资源登记(比如记录日志、更新计数器),要意识到它并不总被执行。
总结下来,co_return 是协程世界的“交接班口”:它不负责干活,但决定谁来接活、怎么交、交什么。写协程不是堆语法糖,而是设计一套值的生命周期契约。你每写一行 co_return,都在和 promise_type 签一份协议——协议写清楚了,协程才真正可控;协议模糊了,调试时看到的就全是“值消失了”的幻觉。
下次 co_return 不生效,别急着查 Task 的 await_ready(),先翻出 promise_type,一行行核对:return_value 存在吗?签名匹配吗?值真的被存进去了吗?await_resume() 拿的是同一个地方吗?——协程的确定性,就藏在这些不起眼的成员函数里。


还没有评论,来说两句吧...