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 兼容 | ✅ | |
| 需要继承、多态 | ✅ |
八、自查清单
- 是否用
[&]捕获了一个生命周期短于闭包的对象? - 回调注册、异步任务中,
this是否可能已销毁? - 返回 lambda 时,是否有引用捕获?
- 捕获了
unique_ptr/大对象时,是否用初始化捕获移动而不是拷贝? - 是否存在
[=]只为省事却捕获了裸指针(指针拷贝不延长生命周期)?
记住一句话:按值捕获拷贝的是指针本身,不是它指向的对象。这是所有 lambda 悬空问题的总根源。