前言

最近打算用 C++ 再刷一刷力扣,但看了一些题的官方解答居然发现很多 C++ 的语法都看不懂,因此准备陆续将 C++ 中一些之前没有怎么学的知识点记录一下。

lambda

lambda 的定义

c++ 在 c++11 标准中引入了 lambda 表达式,一般用于定义匿名函数,使得代码更加灵活简洁。

一个 lambda 表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个 lambda 具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda 可能定义在函数内部。一个 lambda 表达式具有如下形式:

1
[ capture list ] (parameter list) -> return type { function body }

其中,capture list(捕获列表)是一个 lambda 所在函数中定义的局部变量的列表(通常为空);return type、parameter list 和 function body 与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda 必须使用尾置返回来指定返回类型。

我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体,如:

1
auto f =[] { return 42; };

此例中,我们定义了一个可调用对象 f,它不接受参数,返回 42。
lambda 的调用方式与普通函数的调用方式相同,都是使用调用运算符。

1
cout << f() << endl; // 打印42

在 lambda 中忽略括号和参数列表等价于指定一个空参数列表。在此例中,当调用 f 时,参数列表是空的。如果忽略返回类型,lambda 根据函数体中的代码推断出返回类型。如果函数体只是一个 return 语句,则返回类型从返回的表达式的类型推断而来。否则,返回类型为 void。

向 lambda 传参

与一个普通函数调用类似,调用一个 lambda 时给定的实参被用来初始化 lambda 的形参。通常,实参和形参的类型必须匹配。但与普通函数不同,lambda 不能有默认参数。因此,一个 lambda 调用的实参数目永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。

如下面这个简单的求两数之和的 lambda 表达式:

1
2
3
4
5
6
void test1() {
auto f = [] (int a = 1, int b = 2) { return a + b; };

cout << f() << endl; // 输出 3
cout << f(3, 4) << endl; // 输出 7
}

很奇怪的是虽然 C++ primer 与其他很多地方都说 lambda 不能有默认参数,但在 g++ 8.1.0 与 C++11 的环境下居然是可以的,不知道为什么。

使用捕获列表

虽然一个 lambda 可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。一个 lambda 通过将局部变量包含在其捕获列表中来指出将会使用这些变量。捕获列表指引 lambda 在其内部包含访问局部变量所需的信息。

当定义一个 lambda 时,编译器生成一个与 lambda 对应的新的(未命名的)类类型。可以这样理解,当向一个函数传递一个 lambda 时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用 auto 定义一个用lambda 初始化的变量时,定义了一个从 lambda 生成的类型的对象。

默认情况下,从 lambda 生成的类都包含一个对应该 lambda 所捕获的变量的数据成员。类似任何普通类的数据成员,lambda 的数据成员也在 lambda 对象创建时被初始化。

值捕获

类似参数传递,变量的捕获方式也可以是值或引用。与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在 lambda 创建时拷贝,而不是调用时拷贝

因此,考虑如下例子,由于被捕获的变量在 lambda 创建时就已经拷贝,因此即使后续改变了 a 的值,调用 f 输出的也是 f 被创建时的变量 a 的值。

1
2
3
4
5
6
void test2() {
int a = 42;
auto f = [a] { return a; };
a = 0;
cout << f() << endl; // 输出 42
}

引用捕获

也可以使用引用的方式捕获变量,例如:

1
2
3
4
5
6
void test3() {
int a = 42;
auto f = [&a] { return a; };
a = 0;
cout << f() << endl; // 输出 0
}

a 之前的 & 指出 a 应该以引用方式捕获。一个以引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在 lambda 函数体内使用此变量时,实际上使用的是引用所绑定的对象。在本例中,当 lambda 返回 a 时,它返回的是 a 指向的对象的值。

引用捕获与返回引用有着相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在 lambda 执行的时候是存在的。lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果 lambda 可能在函数结束后执行,捕获的引用指向的局部变量已经消失。

隐式捕获

除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据 lambda 体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个 &=& 告诉编译器采用捕获引用方式,= 则表示采用值捕获方式。

如果我们希望对一部分变量采用值捕获,其他变量采用引用捕获,可以混合使用二者,当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个 &=。此符号指定了默认捕获方式为引用或值。

当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式(使用了 &),则显式捕获命名变量必须采用值方式,因此不能在其名字前使用 &。类似地,如果隐式捕获采用的是值方式(使用了 =),则显式捕获命名变量必须采用引用方式,即,在名字前使用 &

捕获列表 描述
[] 空捕获列表。lambda 不能使用所在函数中的变量。一个 lambda 只有捕获变量后才能使用它们
[names] names 是一个逗号分隔的名字列表,这些名字都是 lambda 所在函数的局部变量。默认情况下,捕获列表中的变量都被拷贝。名字前如果使用了 &,则采用引用捕获方式
[&] 隐式捕获列表,采用引用捕获方式。lambda 体中所使用的来自所在函数的实体都采用引用方式使用
[=] 隐式捕获列表,采用值捕获方式。lambda 体将拷贝所使用的来自所在函数的实体的值
[&,identifier_list] identifier_list 是一个逗号分隔的列表,包含 0 个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list 中的名字前面不能使用 &
[=,identifier_list] identifier_list 中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list 中的名字不能包括 this,且这些名字之前必须使用 &

使用案例

实际上书中关于 lambda 还有很多内容,等遇到的时候再继续记录吧,目前的这些已经够用了,下面列出一个使用案例,用于解 T228.汇总区间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public:
vector<string> summaryRanges(vector<int>& nums) {
vector<string> ans;
auto n = nums.size();
int left = 0, right = 0;

// 这里定义了一个 lambda 对象并使用 引用捕获 的方式捕获函数中的所有局部变量
// 因此在 lambda 内部对于变量的调整才能影响到函数的变量。
auto appendToAns = [&] {
if (left == right) ans.emplace_back(to_string(nums[right]));
else ans.emplace_back(to_string(nums[left]) +
"->" + to_string(nums[right]));
left = right + 1;
return;
};

while (right < n) {
if ((right == n - 1) || (right + 1 < n) &&
(long(nums[right + 1]) - long(nums[right]) != 1))
appendToAns(); // 实际上完全没有必要使用 lambda,这里只是 lambda 瘾犯了,演示一下。
right++;
}

return ans;
}
};