第27章 Lambda表达式
第27章 Lambda表达式
- Lambda表达式的完整语法与捕获列表
- 如何用Lambda替代传统的函数对象
- Lambda在STL算法和多线程中的实战应用
想象一下,你需要一个临时的、只用一次的小工具,比如拧一颗特定尺寸的螺丝。你不会为此专门去买一套完整的工具套装,而是找一个顺手的小扳手。在C++中,Lambda表达式就是这样一个“顺手的小扳手”,它允许你在代码中就地定义一个匿名函数对象,无需预先声明一个独立的函数或函数对象类。
Lambda是什么?
从技术上讲,Lambda表达式是C++11引入的一种语法糖,用于生成一个未命名的函数对象(闭包)。编译器会根据你的Lambda代码,自动生成一个唯一的类类型并实例化它。这使得在需要函数指针、函数对象(仿函数)或`std::function`的地方,可以极其方便地嵌入一段逻辑。
解剖Lambda:从最小形式到完整形态
一个Lambda表达式的基本结构如下,我们通过一个表格来快速理解每个部分:
| 组成部分 | 说明 | 示例 | 是否可省略 |
|---|---|---|---|
| 捕获列表 [ ] | 定义Lambda体可以访问哪些外部变量及访问方式。 | [x, &y] | 否(可为空[]) |
| 参数列表 ( ) | 与普通函数参数列表相同。 | (int a, double b) | 是(无参时可省略) |
| 可变规格 mutable | 允许按值捕获的变量在Lambda体内被修改。 | mutable | 是 |
| 异常说明 noexcept | 声明Lambda是否会抛出异常。 | noexcept | 是 |
| 返回类型 -> | 尾置返回类型声明。 | -> bool | 是(编译器可推导时) |
| 函数体 { } | 包含具体的执行代码。 | { return x > 0; } | 否 |
最简单的Lambda可以只有捕获列表和函数体:[] { std::cout << \"Hello Lambda\"; }。但实际开发中,捕获列表和参数列表是关键。
捕获列表:连接内外世界的桥梁
捕获列表决定了Lambda如何“看到”它所在作用域的变量。这是Lambda最强大也最容易出错的地方。我在项目中踩过这个坑:一个按值捕获的指针在Lambda被异步执行时,其指向的对象可能已被销毁,导致悬空指针访问。
捕获方式主要分为两类:按值捕获和按引用捕获。
int a = 10, b = 20;
// 按值捕获a,按引用捕获b
auto lambda1 = [a, &b]() { return a + b; };
// 默认按值捕获所有外部变量
auto lambda2 = [=]() { return a + b; };
// 默认按引用捕获所有外部变量
auto lambda3 = [&]() { return a + b; };
// 混合捕获:b按引用,其余按值
auto lambda4 = [=, &b]() { return a + b; };
// 混合捕获:a按值,其余按引用
auto lambda5 = [&, a]() { return a + b; };
常见误区
- 悬空引用:Lambda被延迟调用(如放入线程或队列)时,按引用捕获的局部变量可能已离开作用域被销毁。
- 误解mutable:
mutable仅允许修改按值捕获的变量的副本,不影响外部原变量。它不改变捕获方式。 - 捕获全局/静态变量:全局变量、静态局部变量、类的
this指针无需捕获即可直接使用,捕获它们是多此一举。
Lambda如何工作:编译器的魔法
当你写下Lambda时,编译器在背后为你生成一个匿名的类。这个过程可以用一个简单的架构图来理解:
例如,int factor=2; auto l = [factor](int x){ return x * factor; }; 编译器大致会生成如下类:
class __AnonymousLambdaClass {
private:
int factor; // 按值捕获的变量变为成员
public:
__AnonymousLambdaClass(int f) : factor(f) {}
int operator()(int x) const { // 注意默认是const的
return x * factor;
}
};
编者提示:理解Lambda生成的闭包类型是唯一的、匿名的,有助于你理解第28章《多线程编程基础》中如何将Lambda传递给std::thread构造函数,以及第25章《智能指针》中如何用Lambda自定义删除器。
实战场景:Lambda的用武之地
实际开发中,大部分团队会选择用Lambda来简化STL算法的调用和实现回调接口,代码更紧凑,意图更清晰。
STL算法伴侣
std::sort,std::find_if自定义比较或条件std::for_each遍历容器并操作元素- 替代手写的函数对象类
异步与回调
std::thread的线程函数std::async的异步任务- GUI或网络库中的事件处理器
延迟计算
- 生成可调用对象,在需要时才执行逻辑
- 与
std::function结合,实现策略模式
来看一个具体的STL算法例子。假设我们有一个`vectorstd::copy_if的条件一目了然:
struct Employee { string name; double salary; int years; };
vector staff = { ... };
vector candidates;
double salary_threshold = 5000.0;
int year_threshold = 3;
// 使用Lambda,条件逻辑紧邻算法,无需额外定义函数
std::copy_if(staff.begin(), staff.end(),
std::back_inserter(candidates),
[salary_threshold, year_threshold](const Employee& emp) {
return emp.salary > salary_threshold && emp.years > year_threshold;
});
性能考量:Lambda vs 函数指针 vs std::function
Lambda生成的闭包对象,其operator()调用通常是内联的,性能与手写的函数对象类相当,优于通过函数指针的间接调用。而std::function是一个类型擦除的包装器,会带来轻微的开销(通常一次动态分配和虚函数调用),在性能敏感的循环中需谨慎使用。对于简单的回调,直接使用auto接收Lambda是最高效的。
进阶话题:泛型Lambda与初始化捕获
C++14引入了泛型Lambda,其参数可以使用auto,这实际上让Lambda的operator()变成了一个模板函数。这在编写通用代码时非常有用。
// C++14 泛型Lambda
auto adder = [](auto a, auto b) { return a + b; };
cout << adder(1, 2); // 3
cout << adder("hello, ", "world"); // "hello, world"
// C++14 初始化捕获(广义捕获),可以移动捕获只移动类型
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() { // 将ptr移动进捕获列表
return *p;
};
如果你在编译时遇到错误“`error: use of deleted function ‘__lambda_...`”,这通常是因为你试图捕获或复制一个不可复制的对象(如`std::unique_ptr`),却没有使用移动捕获或引用捕获。
动手试一试
- 编写一个Lambda,它捕获一个整数`base`(按值),并接受一个整数`x`作为参数,返回`x + base`的值。使用`std::transform`算法,将一个`vector
`中的每个元素都加上这个`base`值。 - 创建一个`vector
`,包含一些单词。使用`std::sort`和一个Lambda,按照单词的长度进行升序排序。如果长度相同,则按字典序排序。 - (挑战)使用C++14的初始化捕获,将一个`std::unique_ptr
`移动捕获到一个Lambda中,并在Lambda体内打印其值。思考为什么这里必须用移动捕获。
检验你的理解
- 以下代码中,变量`local`在`func()`返回后已被销毁,那么`task`被调用时会发生什么?
void schedule(std::functiontask);
void func() {
int local = 100;
schedule([&local](){ cout << local; });
} - 写出一个Lambda表达式,它不捕获任何变量,接受两个`int`参数,返回它们的乘积。
- 如果你想在Lambda内部修改一个按值捕获的变量`counter`,需要在Lambda的哪个部分添加什么关键字?
本章小结
- Lambda本质:是编译器生成的匿名函数对象,语法为`[捕获](参数) mutable noexcept -> 返回类型 {函数体}`。
- 捕获列表是关键:按值`[var]`创建副本,按引用`[&var]`创建别名。需警惕悬空引用,尤其是Lambda被延迟执行时。
- 主要应用场景:与STL算法结合定义临时逻辑、作为线程或异步任务入口、实现简洁的回调函数。
- 性能与类型:Lambda调用通常可内联,效率高。其类型唯一,可用`auto`接收或转换为`std::function`(有开销)。
- 新特性:C++14的泛型Lambda(`auto`参数)和初始化捕获(`[var = expr]`)提供了更强的表达能力。
微信扫码关注
抖音扫码关注