为何Lambda按值捕获无法在C++协程中正常工作?
问题背景
我实现了一个C++协程框架,支持通过co_await调用自定义协程并编排执行序列,示例代码如下:
co_await obj->move_to(....) obj->face(LEFT); co_await wait(15);
协程由主程序逐帧更新,按顺序执行指令,效果如同阻塞调用。
协程实现代码
struct Script { struct suspend_maybe { bool suspend; bool await_ready() noexcept { return !suspend; } void await_suspend(std::coroutine_handle<> c) noexcept {} void await_resume() noexcept { } }; struct promise_type { std::coroutine_handle<promise_type> child; std::coroutine_handle<promise_type> caller; bool cancelled = false; void cancel() { cancelled = true; } Script get_return_object() { return { std::coroutine_handle<Script::promise_type>::from_promise(*this) }; } suspend_maybe initial_suspend() noexcept { return { !caller }; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() {} void return_void() {} }; bool await_ready() noexcept { return false; } bool await_suspend(std::coroutine_handle<Script::promise_type> c) noexcept { c.promise().child = h_; h_.promise().caller = c; return true; } void await_resume() noexcept { } std::coroutine_handle<Script::promise_type> h_; Script(std::coroutine_handle<Script::promise_type> h) :h_{ h } { } operator std::coroutine_handle<Script::promise_type>() const { return h_; } // 逐帧更新协程,管理控制流 void update() { auto& child = h_.promise().child; if (child && !(child.done() || child.promise().cancelled)) { child.promise().get_return_object().update(); } else { if (child && (child.done() || child.promise().cancelled)) { child.destroy(); child = nullptr; } h_.resume(); } } }; typedef std::coroutine_handle<Script::promise_type> CHandle;
主循环更新逻辑
CHandle ScriptPlayer::spawn(CHandle script) { scripts.push_back(script); return script; } void ScriptPlayer::update() { for (int i = 0; i < static_cast<int>(scripts.size()); ) { Script{ scripts[i] }.update(); if (play_next_script) break; // 检查协程是否完成或被取消 if (scripts[i].done() || scripts[i].promise().cancelled) { scripts[i].destroy(); scripts.erase(scripts.begin() + i); } else i++; } }
问题场景
使用参数传递指针的Lambda生成协程时运行正常:
auto terminate = [](ScriptPlayer* player, Object* fx) -> Script { co_await fx->wait_animation(); player->destroy_object(fx); }(player, fx); player->spawn(terminate);
但改用按值捕获指针的Lambda时,程序会在wait_animation执行过程中崩溃,协程栈帧完全损坏:
auto terminate = [player, fx]() -> Script { co_await fx->wait_animation(); player->destroy_object(fx); }(); player->spawn(terminate);
我原本以为按值捕获指针与参数传递效果一致,请问问题出在哪里?
问题原因
核心原因是C++协程对Lambda捕获变量的存储逻辑差异:
参数传递的安全逻辑:
当通过函数参数传递指针时,这些指针会被直接拷贝到协程的堆分配帧中。协程框架会管理这个堆帧的生命周期,只要协程未完成,堆帧就会保持有效,后续访问指针不会有问题。按值捕获的风险点:
按值捕获的变量属于Lambda闭包对象的成员。你的协程初始挂起逻辑(initial_suspend返回suspend_maybe{ !caller })会让协程在创建后立即挂起,此时编译器可能不会将捕获的指针拷贝到协程堆帧中——因为代码第一行就是co_await fx->wait_animation(),fx的访问发生在协程挂起前,编译器会直接引用Lambda闭包对象中的fx成员。而Lambda是临时对象,在
terminate变量初始化完成后就会被销毁。当wait_animation完成、协程恢复执行时,会通过已经悬空的Lambda闭包对象引用访问指针,导致非法内存访问,最终引发栈帧损坏和崩溃。
解决方案
可以通过以下方式解决:
显式拷贝到协程局部变量:
在协程开头将捕获的指针拷贝到局部变量,强制编译器将这些变量存入协程堆帧:auto terminate = [player, fx]() -> Script { auto local_player = player; auto local_fx = fx; co_await local_fx->wait_animation(); local_player->destroy_object(local_fx); }();调整协程初始挂起行为:
将promise_type的initial_suspend改为返回std::suspend_never,让协程创建后立即执行到第一个挂起点,此时编译器会自动将需要的变量拷贝到堆帧,Lambda临时对象销毁后不会影响协程执行。但这个修改会改变协程的初始执行逻辑,需要结合业务场景调整。
内容的提问来源于stack exchange,提问作者Lake

