C++20协程中co_yield指向局部变量的指针是否不安全?
结论
你的写法本身存在根本性的生命周期错误,和最开始返回栈变量指针的示例属于同一类违规,不是generator实现的bug,ASAN报出的use-after-free是符合预期的未定义行为。
普通返回栈指针的错误本质
先看最开始的普通函数示例:
int* foo() { int i = 0; return &i; } int main() { int* p = foo(); }
普通函数执行到return语句返回时,对应的函数栈帧会被直接回收,栈上存储的局部变量i生命周期结束,返回的地址指向已经被释放的内存,p自然成为悬空指针,这是C++明确规定的未定义行为。
协程场景下的生命周期规则
C++20协程的执行逻辑和普通函数有区别,但生命周期违规的本质没有变:
- 协程执行到
co_yield/co_await挂起时,不会像普通函数返回那样销毁当前执行上下文,协程体内定义的局部变量(比如你示例里的array<byte,4096> b)会被保存在堆上分配的协程状态对象中,直到协程完全执行结束(走到co_return、执行完协程体、抛出未捕获异常)才会随协程状态一起析构销毁。 - 但这不代表你可以安全返回指向协程局部变量的指针/视图,你的代码踩中了协程场景下最常见的生命周期坑:
- 你
co_yield的span<byte>是纯粹的视图类型,本身不持有内存,内部只存了指向缓冲区b的指针和长度,完全依赖b的存活保证有效性。 - 所有主流generator实现(包括你用的
boost::asio::experimental::coro、C++23标准std::generator、cppcoro等库的实现)都只做一个保证:在协程处于挂起状态、还没调用迭代器operator++()恢复协程执行之前,通过operator*()拿到的yield值是有效的。一旦你恢复协程执行、或者协程执行完毕,协程状态可能被释放、或者内部局部变量被覆盖修改,之前拿到的span里保存的指针就会失效。 - 尤其是Asio的实验性协程本身是为异步场景设计的,在挂起、恢复的流程中可能转移协程状态的内存位置、甚至提前释放不再需要的协程内存块,你保存在span里的旧地址直接指向被释放的内存,自然会触发ASAN报错。
- 你
修正方案
不要在协程中yield指向协程内部局部变量的指针、引用、视图类对象,两种常见的安全写法:
- 方案1:把缓冲区定义在协程外部,生命周期覆盖整个遍历周期,作为参数传入协程,yield的span指向外部缓冲区:
generator<span<byte>> read(stream& s, span<byte> out_buf) { while (s.is_open()) { const size_t n = s.read_some(out_buf); co_yield out_buf.first(n); } } int main() { stream s; array<byte, 4096> b; // 缓冲区生命周期覆盖整个for循环,不存在悬空风险 for (span<byte> v : read(s, b)) { // 安全使用v } }
- 方案2:让yield的对象本身持有数据内存,比如直接返回支持移动语义的缓冲区对象、或者把缓冲区存在generator的promise成员中,保证数据生命周期和迭代器拿到的值的生命周期绑定。
内容的提问来源于stack exchange,提问作者user118534
相关产品推荐
相关产品推荐

