协程函数被编译器重写为状态机的触发条件是函数体内出现co_await、co_yield或co_return任一关键字,此时编译器在编译期将其重写为带状态跳转逻辑的类成员函数,而非运行时行为。

协程函数被编译器重写成状态机的触发条件
只要函数体内出现 co_await、co_yield 或 co_return 中任意一个关键字,编译器就认定它是协程函数,并启动状态机生成流程。这不是运行时行为,而是编译期强制转换——函数签名不变,但函数体被完全重写为一个带状态跳转逻辑的类成员函数(通常叫 actor 或 resume)。
常见错误现象:写了 co_await 却没生效,比如忘记加 -fcoroutines 编译选项,或 C++ 标准未设为 c++20;此时编译器直接报错,不会静默降级为普通函数。
- 必须启用
-fcoroutines(GCC/Clang)或/await(MSVC) - 必须指定
-std=c++20或更高标准 - 函数返回类型需满足协程要求:必须嵌套定义
promise_type
协程帧(coroutine frame)的布局与分配时机
协程帧是编译器在堆上(默认)分配的一块连续内存,用于保存所有局部变量、参数副本、当前状态编号(state enum)、promise 对象和 coroutine_handle 的元信息。它不是栈帧,也不复用调用者的栈空间——这是“无栈协程”的核心含义。
性能影响明显:每次协程创建都伴随一次堆分配;若频繁短生命周期协程(如每请求一个),可能成为瓶颈。部分编译器支持 __builtin_coro_alloc 优化,但需手动干预且不可移植。
立即学习“C++免费学习笔记(深入)”;
- 默认分配在堆上,由
operator new完成,可被promise_type::get_return_object_on_allocation_failure拦截 - 局部变量不再存于栈,全部序列化进协程帧;引用、指针若指向栈变量,挂起后恢复时会悬垂
- 状态编号从 0 开始,每个
co_await点对应一个新状态值,编译器用switch跳转驱动恢复
promise_type 如何参与状态机组装
promise_type 不是用户“实现协程”的工具,而是编译器生成状态机代码时的“接口胶水”。它不控制逻辑,只提供钩子:编译器在固定位置插入对 initial_suspend()、yield_value(T)、final_suspend() 等成员的调用,把用户定义的行为织入状态流转中。
容易踩的坑:误以为 promise_type 是协程主体,于是把业务逻辑全塞进里面;实际上它只应负责状态传递、资源绑定和生命周期信号,复杂逻辑仍应在协程函数体内写。
-
get_return_object()必须返回能持有coroutine_handle的对象(如自定义Task或Generator) -
initial_suspend()返回std::suspend_always表示协程创建后不自动执行,需显式handle.resume() -
final_suspend()若返回std::suspend_never,协程结束后帧立即释放,handle变成悬垂指针
挂起点(suspend point)如何映射为状态跳转
每个 co_await expr 编译后,等价于三步原子操作:调用 expr.await_ready() → 若为 false,则调用 expr.await_suspend(handle) 并返回 → 控制权交还调用者,当前状态编号存入协程帧 → 下次 handle.resume() 时,从该编号进入 switch 分支继续执行。
关键细节:挂起不是“让线程睡一会”,而是函数提前 return;恢复也不是“唤醒”,而是再次调用同一个 actor() 函数,靠状态编号决定从哪行往下走。
-
await_suspend接收的是coroutine_handle,不是原始协程对象;传错 handle 会导致恢复到错误协程帧 - 若
await_ready()返回 true,整个co_await行被优化为同步执行,不产生挂起开销 - 多个
co_await嵌套时,状态编号递增,但帧内只存最新一个;中间状态不保留
promise_type 的析构时机是实际落地中最易出错的部分——它们不遵循 RAII 直觉,而依赖 final_suspend 和手动 destroy() 配合。稍有疏忽,就会触发 use-after-free 或 double-destroy。


















