C++ lambda 捕获与状态
更新时间:2026-08-27。本文是
languages/cpp/主题高手层第 15 篇。入门篇讲了 lambda 的基本写法,这篇深入"捕获"——lambda 是怎么把外面的变量带进来的?值捕获和引用捕获有什么区别?捕获后变量还能不能改?闭包的声明周期有多长?这些问题的答案,决定了 lambda 会不会成为你的下一个 bug 来源。
本文要回答的问题
[=]和[&]的本质区别是什么?什么时候必须用引用捕获?- 值捕获的变量为什么是 const 的?
mutable什么时候需要? [this]捕获有什么坑?- C++14 的"初始化捕获"解决什么问题?
- lambda 闭包对象的内存占用和性能开销多大?
lambda 是"对象",捕获是"成员变量"
lambda 的底层就是一个匿名类 + 一个对象(闭包)。捕获列表决定这个对象里存了什么成员:
int base = 10;
auto f = [base](int x) { return base + x; }; // 值捕获编译器大致生成:
class CompilerGenerated {
int base_; // 捕获的变量变成成员
public:
int operator()(int x) const { return base_ + x; }
};
CompilerGenerated f{10}; // 闭包对象关键认知:闭包对象 = 捕获的变量 + operator()。 lambda 的每个捕获都对应一个成员变量,auto f = [...] 就是构造了这个对象。
值捕获 vs 引用捕获
| 对比项 | 值捕获 [x] | 引用捕获 [&x] |
|---|---|---|
| 闭包存储 | 变量副本 | 变量引用 |
| 捕获时机 | 创建闭包时拷贝 | 不拷贝,用现成的 |
| 原变量修改 | 闭包不受影响 | 闭包看到最新值 |
| 生命周期 | 独立,安全 | 依赖原变量存活 |
| 开销 | 一次拷贝 | 无拷贝 |
int x = 1;
auto by_value = [x]() { return x; };
auto by_ref = [&x]() { return x; };
x = 99;
by_value(); // 1(创建时拷贝)
by_ref(); // 99(看当前值)引用捕获的致命坑:闭包比原变量活得久 → 悬空引用。
std::function<int()> make_f() {
int local = 42;
return [&local]() { return local; }; // 危险!
} // local 已销毁,闭包里的引用悬空
经验:lambda 要存起来(放进容器/返回/传异步)时,用值捕获;只在本作用域同步调用,才考虑引用捕获。
值捕获是 const 的:mutable 的由来
值捕获的成员默认是 const(operator() 是 const 的),所以 lambda 内不能修改值捕获的变量:
int count = 0;
// auto inc = [count]() { ++count; }; // 编译错误!count 是 const
auto inc = [count]() mutable { ++count; }; // mutable 允许修改
inc(); inc();
// count 仍是 0——改的是闭包里的副本| 写法 | 能否修改捕获 | 原变量是否受影响 |
|---|---|---|
[x] | 否(const) | 否 |
[x] mutable | 能 | 否(改副本) |
[&x] | 能 | 是 |
注意 mutable 只影响闭包内的副本,原变量始终不受影响(值捕获本来就是拷贝)。
捕获 this:最常见的隐式捕获
类成员函数里写 lambda,捕获 [=] 或 [this] 会把 this 指针带进去:
class Counter {
int count_ = 0;
public:
void run() {
auto f = [this]() { ++count_; }; // 捕获 this,才能访问成员
f();
}
};坑:[this] 是"按指针捕获"——对象销毁后,闭包里的 this 悬空。
std::function<void()> get_task() {
auto obj = std::make_unique<Counter>();
return [this]() { /* this 指向已销毁对象! */ };
}C++17 提供 [*this] 按值捕获整个对象副本,安全性更好:
auto f = [*this]() { ++count_; }; // 拷贝整个对象,安全C++14 初始化捕获(广义捕获)
初始化捕获(init-capture)允许在捕获列表里直接定义变量,解决了"不能移动捕获"的问题:
auto p = std::make_unique<int>(42);
// C++14:移动捕获(把 unique_ptr 移进闭包)
auto task = [p = std::move(p)]() { return *p; };
// p 已被移走,task 独占
// 直接构造捕获变量
auto f = [n = 10]() { return n * 2; };| 场景 | C++11 的困境 | C++14 的解法 |
|---|---|---|
| 移动捕获 unique_ptr | 只能引用捕获(生命周期坑) | [p = std::move(p)] |
| 计算后捕获 | 先算再捕获,麻烦 | [v = expensive()] |
| 捕获成员表达式 | 不能 | [x = obj.x] |
初始化捕获是现代 C++ 里"把资源安全带进闭包"的标准手段,配合智能指针尤其常用。
泛型 lambda 与闭包开销
6.1 泛型 lambda(C++14)
auto print = [](auto x) { std::cout << x << "\n"; };
print(42); // 实例化为 int 版本
print(3.14); // 实例化为 double 版本auto 参数 = 函数模板的 template<typename T>,每个类型生成一个 operator() 实例。
6.2 闭包的内存与性能
| 问题 | 答案 |
|---|---|
| 闭包对象多大? | 等于捕获的成员之和(值捕获 n 个变量 = n 个成员大小) |
| 无捕获的 lambda? | 空类,可转成函数指针(+ 一元加号技巧) |
| 性能开销? | 内联后零开销(和手写函数一样) |
| 何时有开销? | 存进 std::function(类型擦除,堆分配 + 虚调用) |
// 空捕获 lambda → 可转函数指针,零存储
void (*fp)(int) = [](int x) { std::cout << x; };
// std::function 有类型擦除开销,热路径慎用
std::function<void(int)> f = [](int x) { /* ... */ };工程经验:lambda 在 std::sort、std::for_each 等模板算法里内联展开,零开销;但塞进 std::function 会引入堆分配与间接调用,性能敏感路径避免。
与本站性能主线衔接
- 性能对比:lambda vs 函数对象 vs 函数指针在排序/算法里的性能差异,可用 perf 实测。
- std::function 开销:类型擦除的堆分配与虚调用是热路径隐患,衔接 缓存局部性。
- 算法库:lambda 是 标准库算法 的常用配菜。
- 编译期零成本:模板内联让 lambda 与手写代码等价,与 模板 的零成本抽象一致。
一句话总结
lambda 是带状态的闭包对象:值捕获存副本(const 不可改,mutable 改副本)、引用捕获看最新值但怕悬空、[this] 按指针捕获有生命周期坑、C++14 初始化捕获解决移动捕获;模板算法里用 lambda 零开销,std::function 才有额外成本。