You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Lambda按值捕获无法在C++协程中正常工作?

C++协程框架Lambda按值捕获指针崩溃问题

问题背景

我实现了一个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捕获变量的存储逻辑差异:

  1. 参数传递的安全逻辑:
    当通过函数参数传递指针时,这些指针会被直接拷贝到协程的堆分配帧中。协程框架会管理这个堆帧的生命周期,只要协程未完成,堆帧就会保持有效,后续访问指针不会有问题。

  2. 按值捕获的风险点:
    按值捕获的变量属于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 09:30:58