C++ 补完计划(一):lambda
前言
最近打算用 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 | void test1() { |
很奇怪的是虽然 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 | void test2() { |
引用捕获
也可以使用引用的方式捕获变量,例如:
1 | void test3() { |
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 | class Solution { |