跳到主要内容

Lambda 表达式捕获与生命周期陷阱

lambda 写起来太顺手,顺手到很多人忘了它是一个闭包对象:捕获的东西是它的成员,随它的生命周期存在。绝大多数 lambda 崩溃都源于一件事——闭包活得比被捕获的变量久。

一、语法结构​

[captures](params) mutable -> RetType { body }
  • captures:捕获列表,决定闭包里"多出哪些成员";
  • params:参数列表(C++14 起可用 auto 做泛型 lambda);
  • mutable:允许修改按值捕获的成员(闭包的 operator() 默认是 const);
  • ->RetType:显式返回类型,多数场景可省略。

二、三种捕获方式​

int a = 1, b = 2;

auto f1 = [a, b] { return a + b; }; // 按值:拷贝一份,改动不影响外部
auto f2 = [&a, &b] { a++; return b; }; // 按引用:共用原对象
auto f3 = [=] { return a + b; }; // 隐式按值捕获用到的变量
auto f4 = [&] { a++; }; // 隐式按引用捕获用到的变量
auto f5 = [=, &a] { return a + b; }; // 混合:默认按值,a 按引用

几条硬规则:

  • [=, &a] 合法写法,但 [&, a] 之外的"默认 + 覆盖"顺序只能是默认在前;
  • C++20 起 [=] 不允许隐式捕获 this,需显式写 [this, =] 或 [=, this];
  • 按值捕获在创建时求值一次,之后不再跟随外部变量变化。
int x = 1;
auto f = [x] { return x; };
x = 99;
f(); // 仍返回 1

三、悬空引用的三大场景​

场景 1:返回引用了局部变量的 lambda​

std::function<int()> bad() {
int local = 42;
return [&local] { return local; }; // ❌ local 已销毁
}

std::function<int()> good() {
int local = 42;
return [local] { return local; }; // ✅ 拷贝进闭包
}

场景 2:lambda 存进容器/回调,捕获了作用域变量​

std::vector<std::function<void()>> callbacks;
{
int counter = 0;
callbacks.push_back([&counter] { ++counter; }); // ❌ counter 已出作用域
}
callbacks[0](); // 未定义行为

修复:改成按值捕获,或把状态放进 shared_ptr:

auto state = std::make_shared<int>(0);
callbacks.push_back([state] { ++(*state); }); // ✅ 引用计数保证存活

场景 3:异步任务捕获 this​

class Worker {
public:
void start() {
std::thread([this] { runLoop(); }).detach(); // ❌ Worker 先析构则悬空
}
private:
void runLoop();
};

修复方式按框架不同:用 weak_ptr 判活,或改用 enable_shared_from_this 延长生命周期,或在析构时 join 线程。

四、C++14 初始化捕获(广义捕获)​

C++14 允许在捕获列表里定义新变量,这对移动语义尤其重要:

auto p = std::make_unique<Foo>();
auto f = [p = std::move(p)] { p->run(); }; // 把 unique_ptr 移进闭包

也常用于 C++11 的"按值捕获 + 改名"工作around:

int x = 1;
auto g = [y = x + 1] { return y; }; // 捕获表达式结果

C++11 环境没有这个能力,必须先在外部建一个变量再按值捕获。

五、mutable 的真实含义​

int n = 0;
auto f = [n]() mutable { return ++n; };
f(); f();
// 返回 2,但外部 n 仍为 0——修改的是闭包成员副本

mutable 只影响闭包内部那份副本,不会反向影响外部。若外部变量本身按引用捕获,则无需 mutable 也能改。

六、std::function 与模板的性能差异​

std::function 有类型擦除开销(可能堆分配),模板参数则完全内联:

// 泛型:零开销,适合热点路径
template <class F>
void forEach(const std::vector<int>& v, F f) {
for (const auto& x : v) f(x);
}

// 类型擦除:适合存进容器或跨接口传递
std::function<void(int)> fn = [](int x) { /* ... */ };

经验:能内联就内联,需要存储(回调注册表、std::map 值)时才用 std::function。

七、lambda 与函数对象的选择​

需求选 lambda选函数对象
一次性回调✅
需要多个成员函数与状态✅
需要在头文件中作为模板实参反复使用✅
需要 C++98 兼容✅
需要继承、多态✅

八、自查清单​

  1. 是否用 [&] 捕获了一个生命周期短于闭包的对象?
  2. 回调注册、异步任务中,this 是否可能已销毁?
  3. 返回 lambda 时,是否有引用捕获?
  4. 捕获了 unique_ptr/大对象时,是否用初始化捕获移动而不是拷贝?
  5. 是否存在 [=] 只为省事却捕获了裸指针(指针拷贝不延长生命周期)?

记住一句话:按值捕获拷贝的是指针本身,不是它指向的对象。这是所有 lambda 悬空问题的总根源。