C++ lambda 捕获完整梳理
目录
[C++ lambda 捕获完整梳理](#C++ lambda 捕获完整梳理)
[1. C++11 四种基础捕获](#1. C++11 四种基础捕获)
[1. 值捕获 x](#1. 值捕获 [x])
[2. 引用捕获 \&x](#2. 引用捕获 [&x])
[3. 默认全部值捕获 =](#3. 默认全部值捕获 [=])
[4. 默认全部引用捕获 \&](#4. 默认全部引用捕获 [&])
[2. C++14 新增:初始化捕获 init‑capture(重点,前面聊过move捕获)](#2. C++14 新增:初始化捕获 init‑capture(重点,前面聊过move捕获))
[3. this捕获,捕获类成员](#3. this捕获,捕获类成员)
[4. mutable 干什么](#4. mutable 干什么)
[5. 常见坑总结](#5. 常见坑总结)
[6. 总结](#6. 总结)
[lambda ≈ C++ 的闭包,但不是完全等价](#lambda ≈ C++ 的闭包,但不是完全等价)
lambda语法格式:
[捕获子句](参数) mutable ->返回类型 {函数体};
捕获写在[]里面,作用:把外部作用域的变量,带入lambda内部使用。
注意:C++11、C++14有差异,C++14新增初始化捕获(init‑capture)。
1. C++11 四种基础捕获
1. 值捕获 [x]
把外部变量拷贝一份进lambda内部,lambda内部是副本,外部原变量不受影响。
int a = 10;
auto f = [a](){
// a 是拷贝出来的副本
return a;
};
a = 20;
f(); // 返回10,不受外部a修改影响
默认lambda的
operator()是const,值捕获的副本默认不能修改 ,要改需要加mutable
auto f = [a]() mutable {
a = 100; // 修改的是lambda内部副本,外部a不变
return a;
};
2. 引用捕获 [&x]
捕获外部变量的引用,lambda直接操作外部原本变量,没有拷贝。
int a =10;
auto f = [&a](){
a = 99;
};
f();
// 外部a已经变成99
⚠️巨大坑:引用捕获保存的是引用,如果lambda生命周期长于外部变量,会产生悬空引用,UB崩溃。
3. 默认全部值捕获 [=]
[=]:lambda用到什么外部变量,就自动做值拷贝。
int a=1,b=2;
auto f = [=](){ return a+b; };
4. 默认全部引用捕获 [&]
[&]:用到的外部变量全部引用捕获。
int a=1,b=2;
auto f = [&](){ a = 100; };
混合捕获(部分引用、部分值)
int a=10,b=20;
auto f = [=, &a](){
// a引用;b值拷贝
};
auto f2 = [&, b](){
// &代表其余引用;b值拷贝
};
规则:
[=, &x]大部分值,个别引用;[&, x]大部分引用,个别值。 不能同时写[=,&],两个默认不能一起开。
2. C++14 新增:初始化捕获 init‑capture(重点,前面聊过move捕获)
语法:[变量名 = 表达式] 在lambda内部创建成员变量,表达式可以是任意代码,可以std::move。
std::unique_ptr<int> p = std::make_unique<int>(100);
// val是lambda内部新成员,p移动进去
auto f = [val = std::move(p)](){
return *val;
};
// p已经被move走,外部p=nullptr
-
val = std::move(p):不是引用,不是拷贝,内部构造val,执行move -
可以写任意表达式,不一定move
auto f = [x = 1+2](){ return x; };
⚠️C++11不支持这种写法!C++11只能捕获已经存在的外部变量,不能在
[]里面做赋值、move。
3. this捕获,捕获类成员
struct Demo {
int v = 10;
void test() {
auto f = [this](){
return this->v; // 捕获this指针,访问成员
};
f();
}
};
C++17还新增[*this],把整个对象拷贝一份进lambda,而不是捕获指针this。
auto f = [*this](){ return v; }; //拷贝整个对象,不怕对象析构悬空
4. mutable 干什么
lambda默认生成的operator() const,捕获到的内部成员不能修改。
- 值捕获副本想修改;init‑capture移动进来的成员想修改,需要加
mutable。
auto f = [val = std::move(p)]() mutable {
*val = 200;
return *val;
};
不加mutable编译报错。
5. 常见坑总结
-
引用捕获
[&],lambda生命周期超过外部变量 → 悬空引用,崩溃,异步回调非常容易踩。 -
[=]值捕获:捕获指针,只是拷贝指针,不是拷贝指针指向内存。
int* p = new int(10);
auto f = [=](){ return *p; };
delete p;
f(); // 悬空!拷贝的只是指针p,不是堆上数据
-
unique_ptr不能拷贝,C++11无法直接捕获;C++14用
[val = std::move(p)]移动捕获。 -
[this]捕获的是原始对象指针,如果对象销毁,lambda执行就悬空。C++17优先[*this]拷贝对象。
速查表
| 写法 | 含义 | C++版本 |
|---|---|---|
[x] |
值拷贝变量x | C++11 |
[&x] |
引用变量x | C++11 |
[=] |
用到的变量全部值拷贝 | C++11 |
[&] |
用到的变量全部引用 | C++11 |
[val = expr] |
初始化捕获,内部构造成员,支持move | C++14 |
[this] |
捕获对象指针 | C++11 |
[*this] |
拷贝整个对象 | C++17 |
简单记忆
-
&→ 引用(操作原变量,小心悬空) -
不带
&→ 值拷贝/副本 -
xxx = ...→ C++14初始化捕获,在lambda内部新建成员。
6. 总结
lambda ≈ C++ 的闭包,但不是完全等价
闭包:函数 + 捕获的外部环境变量。 C++ lambda 编译器会生成一个匿名类(闭包对象),捕获的变量变成这个类的成员变量。
两种捕获本质
- 值捕获
[a]/ C++14 初始化捕获[x = ...]把变量拷贝 /move 进闭包对象内部。
闭包对象自己持有一份数据;就算原来栈上变量销毁,闭包不受影响,安全。
- 引用捕获
[&a]、[&]闭包内部存的是引用 / 指针指向外部栈变量 ,不拷贝数据。
⚠️巨大风险:如果 lambda(闭包对象)活得比栈上局部变量更久,栈变量已经出作用域销毁,闭包拿着悬空引用,访问就是未定义行为,随机崩溃。
std::function<void()> bad_case() {
int local = 100;
auto f = [&local](){
printf("%d\n", local); // 危险!引用栈上local
};
return f; // 返回闭包,函数退出,local栈内存销毁
}
// 外面调用,此时local早已销毁,悬空UB
auto fn = bad_case();
fn();
上面这个是 C++ 开发最经典的坑。
如果改成值捕获
[local],就安全:把 local 拷贝进 lambda 对象,函数退出栈变量销毁不影响闭包副本。
补充几个关键点
-
lambda 只是语法;闭包对象可以存到 std::function 里面,延长生命周期,这也是悬空引用问题高发场景。
-
[=]默认值捕获:如果捕获的是指针,只是拷贝指针值,不会拷贝指针指向堆内存,依然会悬空。
int* p = new int(10);
auto f = [=](){ return *p; };
delete p;
f(); // 依然悬空,拷贝的只是指针,不是*p的数据
[this]捕获的是对象指针,对象析构之后再调用 lambda,同样悬空。C++17 的[*this]会把整个对象拷贝进去,可以规避这个问题。
和其他语言闭包简单对比
-
JS/Python 闭包:引用外部变量,语言有 GC,变量不会被提前销毁。
-
C++ 没有 GC;栈变量自动销毁,引用捕获闭包不会延长栈变量生命周期,这是和 GC 语言最大区别。
简短总结
lambda 就是 C++ 实现闭包的语法; ✅值捕获:把数据搬进闭包对象,安全; ⚠️引用捕获只是拿引用,不会延长栈变量生命周期;一旦 lambda 晚于栈变量销毁,直接悬空 UB,异步回调、返回 lambda 的时候一定要高度警惕。
工程小习惯:异步回调、要保存起来延后执行的 lambda,尽量少用[&]默认引用捕获。