← 目录 / 第八章 · 函数 / 8.4 Lambda 表达式
有些函数只会在一个地方用一次,专门给它取个名字、写在别处,反而显得麻烦。本节介绍一种可以"就地"写出来、不需要名字的函数——lambda 表达式。
8.4.1
还记得 11.13.1 节里,给 sort 传第三个参数来自定义排序规则吗?如果想按降序排序,一种办法是专门写一个具名函数:
Cmp 这个函数只在这一次 sort 调用里用得上,别的地方根本不会再调用它。为它专门取一个名字、写在主函数外面,读代码的人还得跳过去才能看懂它是干什么的——有点"杀鸡用牛刀"。
Lambda 表达式(Lambda Expression)就是 C++11 引入的"匿名函数"写法:不需要取名字,可以直接写在使用它的地方,逻辑和调用挨在一起,一眼就能看懂。
8.4.2
Lambda 表达式看起来和普通函数不太一样,但拆开来看,组成部分是相通的:
| 组成部分 | 说明 | 是否必须 |
|---|---|---|
捕获列表 [ ] | 决定这个 lambda 能不能、以及怎样使用外部(lambda 所在函数里)的变量,详见 8.4.3 | ✅ 必需(不需要用外部变量时写 []) |
参数列表 ( ) | 和普通函数的参数列表写法完全一样,调用时传入的数据 | 可选(无参数时可以省略括号里的内容,甚至连括号也可以省略) |
返回类型 -> 类型 | 函数体只有一条 return 语句时,编译器能自动推断出返回类型,通常可以省略;逻辑复杂、有多条不同类型的 return 时才需要手动写明 | 通常可省略 |
函数体 { } | 花括号内要执行的具体代码,写法和普通函数体完全一样 | ✅ 必需 |
| 1 | int main() |
| 2 | { |
| 3 | auto add = [](int a, int b) { return a + b; }; // 把 lambda 存进变量 add |
| 4 | cout << add(3, 4); // 像调用普通函数一样调用,输出 7 |
| 5 | return 0; |
| 6 | } |
auto?每一个 lambda 表达式,编译器背地里都会为它生成一个独一无二、没有名字的类型,你没办法把这个类型显式地写出来。auto(见 2.1 节)能让编译器自动帮你推断出这个类型,这也是存储 lambda 时几乎总是搭配 auto 使用的原因。8.4.3
普通函数写在其他函数外面,本来就看不到别的函数里的局部变量,这一点很容易理解(见 2.6 节作用域)。但 lambda 表达式比较特殊:它是写在另一个函数内部的,按理说应该能"顺手"看到周围的变量——但 C++ 出于安全考虑,要求你必须在捕获列表 [ ] 里明确写出"我要用外面的哪个变量",没有写的变量,lambda 内部一律看不到。
| 写法 | 含义 |
|---|---|
| [ ] | 不捕获任何外部变量,lambda 内部只能用自己的参数和局部变量 |
| [x] | 按值捕获变量 x(拷贝一份快照进来,见下方对比) |
| [&x] | 按引用捕获变量 x(和外部的 x 是同一份数据) |
| [=] | 用到的外部变量全部按值捕获,不用逐个列出来 |
| [&] | 用到的外部变量全部按引用捕获,不用逐个列出来 |
| [x, &y] | 混合写法:x 按值捕获,y 按引用捕获 |
| [this] | 捕获当前对象(12 章学到类之后,在成员函数里写 lambda 时会用到) |
[x],更安全。mutable 关键字:[x]() mutable { x++; return x; }。竞赛中较少用到,了解即可。8.4.4
Lambda 表达式最常见的用途,就是给 STL 算法函数临时提供一条规则——大多数场景根本用不到复杂的捕获,一个 [] 就够了:
| 1 | sort(v.begin(), v.end(), |
| 2 | [](int a, int b) { return a > b; }); // 从大到小 |
有些场景排序规则要参考另一个数组,这时捕获列表就派上用场了——正是 11.13.4 节"间接排序"用到的手法:
| 1 | vector<int> data = {40, 10, 30}; |
| 2 | vector<int> idx = {0, 1, 2}; // 待排序的是下标,不是 data 本身 |
| 3 | sort(idx.begin(), idx.end(), |
| 4 | [&](int i, int j) { return data[i] < data[j]; }); // 引用捕获 data |
| 5 | // 排序后 idx = {1, 2, 0},因为 data[1]=10 < data[2]=30 < data[0]=40 |
这里必须用 [&] 引用捕获 data:如果按值捕获,lambda 内部会拷贝一份 data 的副本,虽然结果依然正确,但对于较大的数组来说是一笔不必要的开销;引用捕获则直接复用外部的 data,不产生额外拷贝。
类似地,priority_queue(见 11.7 节)自定义比较规则、unordered_map 自定义 find_if 查找条件等场景,都是 lambda 大展身手的地方——它们的共同点是:逻辑简单、只用一次,不值得专门写一个具名函数。
8.4.5
[](或者压根没写捕获列表里想用的变量),却在函数体里直接用了外面的变量,编译器会直接报错——提示这个变量"没有被捕获"。解决办法很直接:把它加进 [ ] 里,或者偷懒用 [=] / [&] 一次性捕获所有用到的变量。[x] 拷贝的是创建 lambda 那一刻的值,之后外部 x 再变化,lambda 内部看到的还是旧值(见 8.4.3 的对比示例)。如果需要 lambda 始终读到最新值,要改用引用捕获 [&x]。auto。(进阶:如果确实需要把 lambda 存成类的成员、或者作为函数参数类型传递,可以用标准库提供的 std::function<返回类型(参数类型...)> 包装它,需要 <functional> 头文件——竞赛中较少用到,了解即可。)