Lambda捕获列表与高级用法
极难2小纸条如何记住外面的世界?——C++ Lambda 捕获列表完全指南
想象一下:你正在写一个“自动计算零花钱”的小程序。你有一个 for_each 循环,遍历每月零花钱的列表,每次遇到一个数值就累加到外部变量 total 上。但是,Lambda 默认就像是一个“只带自己文具盒”的学生,它看不见桌子外面的东西。如果想让它使用外面的变量,就得通过 捕获列表 给它递一张“借用清单”,告诉它:“这些变量你可以借用,并且规定好是按原样抄一份(值捕获),还是直接看原版(引用捕获)。”
今天我们就来彻底搞懂这张“清单”怎么写、有哪些注意事项,以及如何用它写出简洁又强大的代码。
捕获列表:一张完整的“借用清单”
捕获列表放在 Lambda 表达式的 [] 中,可以混合多种方式。每个方式都有特定的用途,像生活中不同颜色的便签。
| 写法 | 含义 | 生活类比 |
|---|---|---|
[x] | 按值捕获变量 x(拷贝一份到 Lambda 内部) | 你把书借给同学,同学自己抄了一份笔记,改动不影响你的原书 |
[&x] | 按引用捕获变量 x(直接操作原变量) | 同学直接在原书上写批注,改动会体现在你的书上 |
[=] | 按值捕获所有用到的外部局部变量 | 把所有需要的东西都复印一份带进考场 |
[&] | 按引用捕获所有用到的外部局部变量 | 把所有人都叫进考场,直接在原卷上改答案 |
[=, &x] | 默认按值捕获,但 x 按引用 | 除了 x 是直接操作原变量,其他都复印 |
[this] | 捕获当前对象的指针(类成员函数内) | 走进考场前,带上学生证(代表你本人) |
[x = std::move(y)] | 初始化捕获:用 y 移动构造一个新变量 x | 把一支笔送给别人,别人得到笔,你自己没笔了 |
注意:如果没有捕获列表(空
[]),Lambda 只能使用自己的参数和全局变量,不能碰任何外部局部变量。
为什么需要捕获?从两个真实场景入手
场景1:统计超过某个分数的同学人数
你有一列考试成绩,要统计超过 60 分的同学数量。外部变量 limit = 60 和 count = 0 都需要被 Lambda 使用。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> scores = {45, 78, 90, 32, 88, 59};
int limit = 60; // 及格线
int count = 0; // 计数器
for_each(scores.begin(), scores.end(),
[limit, &count](int s) { // limit 值捕获(只读),count 引用捕获(可修改)
if (s > limit) ++count;
});
cout << "及格人数: " << count << endl; // 输出 3 (78,90,88)
return 0;
}
这里 limit 只需要读取,所以用值捕获(拷贝一份,安全且不影响原变量);count 需要修改,所以用引用捕获。
场景2:在成员函数内打印带前缀的信息
你写了一个游戏,每个角色有自己的 name 和 hp。想在函数内过滤出血量大于 0 的角色,并打印。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Character {
string name;
int hp;
public:
Character(const string& n, int h) : name(n), hp(h) {}
void showAlive(const vector<Character>& team) {
// Lambda 需要访问当前对象的 name 前缀?实际上这里需要访问成员吗?
// 更常见的场景:在类成员函数内调用 Lambda,需要访问其他成员
for_each(team.begin(), team.end(),
[this](const Character& c) { // 捕获 this 指针
if (c.hp > 0) {
cout << "成员 " << c.name << " 还活着!" << endl;
}
});
}
};
如果不捕获 this,Lambda 内就无法访问 Character 的私有成员。
常见错误与陷阱
1. 引用捕获导致悬空引用
这是最危险的错误。如果你把 Lambda 存到某个地方,而它引用的局部变量已经销毁,那么 Lambda 调用时访问的就是非法内存。
function<int()> badLambda() {
int x = 42;
return [&x]() { return x; }; // 危险!引用捕获局部变量,函数返回后 x 销毁
}
auto f = badLambda();
cout << f(); // 未定义行为,可能输出垃圾值
解决方法:如果要返回 Lambda,必须使用值捕获,或者用初始化捕获移动。
2. 按值捕获默认不能修改
初学者常犯的错误是认为值捕获后可以随意修改:
int a = 10;
auto lam = [a]() { a++; }; // 编译错误:a 是 const
即使你想修改副本,也必须加上 mutable 关键字。但 mutable 只修改副本,不影响外部。
3. 值捕获大对象导致性能问题
如果你值捕获一个大型对象(如 vector<int> 有 10 万个元素),Lambda 会拷贝一份,开销极大。此时应该用引用捕获或初始化捕获(移动)。
vector<int> bigData(100000, 1);
// 糟糕:拷贝了整个 bigData
auto lam1 = [bigData]() { cout << bigData.size(); };
// 正确:引用捕获,但注意生命周期
auto lam2 = [&bigData]() { cout << bigData.size(); };
// 更好的移动捕获(C++14)
auto lam3 = [data = move(bigData)]() { cout << data.size(); };
// 移动后 bigData 为空,但 lam3 拥有数据
4. 递归 Lambda 无法直接调用自身
因为 Lambda 是匿名类型,在它的体内无法用名字调用自己。需要借助 std::function 或者用 Y 组合子(一般用 std::function 更简单)。
// 错误:lambda 体中无法使用自身的名字
auto factorial = [](int n) { return n <= 1 ? 1 : n * factorial(n-1); };
// 正确:用 std::function 包装
function<int(int)> factorial = [&](int n) -> int {
return n <= 1 ? 1 : n * factorial(n-1);
};
完整可运行示例:模拟计算器
下面这个程序模拟一个简单的计算器,用 Lambda 捕获外部操作次数,并支持多种运算。
#include <iostream>
#include <functional> // for std::function
#include <vector>
using namespace std;
int main() {
// 创建一个计算器:记录操作次数
int operationCount = 0; // 操作次数计数器
// 加法函数 —— 捕获计数器的引用,每调用一次就加1
function<int(int,int)> add = [&operationCount](int a, int b) -> int {
operationCount++;
return a + b;
};
// 乘法 —— 同样捕获计数器
function<int(int,int)> multiply = [&operationCount](int a, int b) -> int {
operationCount++;
return a * b;
};
// 使用 Lambda 作为回调:对列表中的每对数进行操作
vector<pair<int,int>> nums = {{3,4}, {5,2}, {6,7}};
cout << "加法结果: ";
for (auto& p : nums) {
cout << add(p.first, p.second) << " "; // 调用 add,count 递增
}
cout << endl;
cout << "乘法结果: ";
for (auto& p : nums) {
cout << multiply(p.first, p.second) << " ";
}
cout << endl;
cout << "总操作次数: " << operationCount << endl; // 输出 6
// 演示初始化捕获:创建一个带有定制偏移量的加法器
int base = 100;
auto customAdder = [baseValue = base * 2](int x) { // 初始化一个新变量 baseValue
return x + baseValue;
};
cout << "baseValue 初始化为 base*2 = " << base * 2 << ",调用 customAdder(5) = "
<< customAdder(5) << endl; // 输出 205
return 0;
}
Python 版对照
Python 没有捕获列表语法,但闭包(嵌套函数)天然支持捕获外部变量。需要注意 nonlocal 关键字来修改外层变量。
def main():
# 对应 C++ 的引用捕获:使用列表模拟可变对象
operation_count = [0] # 用列表包装,方便在内部修改
def add(a, b):
operation_count[0] += 1
return a + b
def multiply(a, b):
operation_count[0] += 1
return a * b
nums = [(3,4), (5,2), (6,7)]
print("加法结果:", [add(a,b) for a,b in nums])
print("乘法结果:", [multiply(a,b) for a,b in nums])
print("总操作次数:", operation_count[0])
# 初始化捕获类似:创建新的局部变量
base = 100
custom_adder = lambda x: x + base * 2 # 直接使用外部变量,Python 闭包捕获的是引用
# 但注意:Python 的 lambda 捕获的是变量对象本身,如果 base 是 immutable 类型,
# 后续改 base 不会影响已经创建的 lambda,因为捕获的是 base 当前指向的值(按名绑定)
# 更安全的做法:用默认参数冻结
custom_adder2 = lambda x, b=base*2: x + b # 使用默认参数实现“值捕获”
print("custom_adder2(5) =", custom_adder2(5)) # 205
if __name__ == "__main__":
main()
总结与指引
| 要点 | 说明 |
|---|---|
| 捕获列表是 Lambda 连接外部世界的桥梁 | 空[]表示不捕获,[=]全值,[&]全引用,可混合 |
| 值捕获默认只读 | 需要修改副本时加 mutable,但外部变量不变 |
| 引用捕获可修改外部 | 注意生命周期,避免悬空引用 |
| 初始化捕获(C++14) | 允许创建新变量或移动资源,如 [ptr = move(p)] |
| 泛型 Lambda(C++14) | 参数用 auto,像模板一样适应多种类型 |
捕获 this | 类成员函数内访问成员变量/函数 |
| 递归 Lambda | 必须用 std::function 包装 |
接下来可以学习:
std::function与std::bind的用法- 高阶函数与函数式编程思想
- C++17 的 constexpr Lambda
- 生命周期管理:智能指针与 Lambda 配合
掌握了捕获列表,你就能写出像胶水一样黏合上下文与算法的 Lambda,让 STL 算法变得无比灵活。快去用 std::sort 配合捕获一个外部比较规则试试吧!
例题精讲
考虑以下C++代码片段,关于Lambda捕获列表的说法,哪一个是正确的?
在C++中,使用默认捕获模式[=]的Lambda表达式内部可以修改按值捕获的变量,只要在Lambda参数列表后加上mutable关键字。
以下代码试图使用Lambda对vector<int>进行排序,但需要捕获外部变量desc(bool类型)来决定降序还是升序。请补全捕获列表。
std::vector<int> v = {3, 1, 4, 1, 5};
bool desc = true;
std::sort(v.begin(), v.end(), [___](int a, int b) { return desc ? a > b : a < b; });关于Lambda的初始化捕获(C++14起支持),以下哪个用法是正确的?
在C++中,一个Lambda表达式如果捕获了局部变量的引用,那么该Lambda对象可以安全地存储在全局容器中,即使该局部变量已经销毁。