这是对于C++11中新添加的Lambda表达式的学习总结,截至目前遇到过两次这样的题目。可参考:LeetCode-520. Detect Capital、LeetCode-406. Queue Reconstruction by Height、 LeetCode-155. Min Stack。
利用Lambda表达式,可以方便的定义和创建匿名函数。对于C++这门语言来说来说,“Lambda表达式”或“匿名函数”这些概念听起来好像很深奥,但很多高级语言在很早以前就已经提供了Lambda表达式的功能,如C#,Python等。今天,我们就来简单介绍一下C++中Lambda表达式的简单使用。隐约感觉这个好像还和函数式编程有关。所以借此机会学习学习。
参考MSDN中的 C++ 中的 Lambda 表达式、C++ 11 Lambda表达式、C++11 学习笔记 lambda表达式。有必要说明一下,以上参考都是非常优秀完整的博客,我只是针对其内容进行选择性的整理。只用作个人学习积累。
2018-5-14遇到有关Lambda表达式的题目可参考:
Lambda表达式完整的声明格式如下:
[capture list] (params list) mutable exception-> return type { function body }
汉语格式参考:C++ Lambda表达式用法。
[函数对象参数] (操作符重载函数参数) mutable或exception声明 -> 返回值类型 {函数体}
[函数对象参数] (操作符重载函数参数) mutable或exception声明 -> 返回值类型 {函数体}
对应各项含义如下:
参考博客:C++ 11 Lambda表达式,上述含义也可以解释如下:
由于六个部分中有的可选,所以可以根据实际情况进行适当省略。常见格式有如下:
其中:
Lambda表达式可以使用其可见范围内的外部变量,但必须明确声明(明确声明哪些外部变量可以被该Lambda表达式使用)。那么,在哪里指定这些外部变量呢?Lambda表达式通过在最前面的方括号[]来明确指明其内部可以访问的外部变量,这一过程也称过Lambda表达式“捕获”了外部变量。
Lambda 可在其主体中引入新的变量(用 C++14),它还可以访问(或“捕获”)周边范围内的变量。 Lambda 以 Capture 子句(标准语法中的 lambda 引导)开头,它指定要捕获的变量以及是通过值还是引用进行捕获。 有与号 (&) 前缀的变量通过引用访问,没有该前缀的变量通过值访问。
空 capture 子句 [ ] 指示 lambda 表达式的主体不访问封闭范围中的变量。
可以使用默认捕获模式(标准语法中的 capture-default)来指示如何捕获 lambda 中引用的任何外部变量:[&] 表示通过引用捕获引用的所有变量,而 [=] 表示通过值捕获它们。 可以使用默认捕获模式,然后为特定变量显式指定相反的模式。
详细来说,捕获分为值捕获与引用捕获。
1、值捕获
值捕获和参数传递中的值传递类似,被捕获的变量的值在Lambda表达式创建时通过值拷贝的方式传入,因此随后对该变量的修改不会影响影响Lambda表达式中的值。
示例如下:
int main()
{
int a = 123;
auto f = [a] { cout << a << endl; };
a = 321;
f(); // 输出:123
}
需要注意的是:如果以传值方式捕获外部变量,则在Lambda表达式函数体中不能修改该外部变量的值。
2、引用捕获
使用引用捕获一个外部变量,只需要在捕获列表变量前面加上一个引用说明符&。引用捕获的变量使用的实际上就是该引用所绑定的对象。如下:
int main()
{
int a = 123;
auto f = [&] { cout << a << endl; }; // 引用捕获
a = 321;
f(); // 输出:321
}
从上面例子可以看到,int a的初值是123,f的声明在123之后,后来a的值更改为321。这是输出f(),发现a的值发生了改变。这可以与按值捕获相对比,不难发现,按值捕获,a的值改变不会影响f()中的结果,但是按照引用捕获,会影响。
3.隐式捕获
以上两种例子都需要在捕获列表中表面需要捕获的外部变量,但是我们也可以让编译器根据lambda体中需要的代码来推断需要捕获哪些变量。这就是隐式捕获。
隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。
隐式引用捕获示例:
1.使用[=]
int main()
{
int a = 123;
auto f = [=] { cout << a << endl; }; // 值捕获
f(); // 输出:123
}
2.使用[&]
int main()
{
int a = 123;
auto f = [&] { cout << a << endl; }; // 引用捕获
a = 321;
f(); // 输出:321
}
4.综合使用
综合起来,例如,如果 lambda 体通过引用访问外部变量 total 并通过值访问外部变量 factor,则以下 capture 子句等效:
[&total, factor]
[factor, &total]
[&, factor]
[factor, &]
[=, &total]
[&total, =]
解释可参考:
捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。
如果 capture 子句包含 capture-default &,则该 capture 子句的 identifier 中没有任何 capture 可采用 & identifier 形式。 同样,如果 capture 子句包含 capture-default =,则该 capture 子句的 capture 不能采用 = identifier 形式。 identifier 或 this 在 capture 子句中出现的次数不能超过一次。 以下代码片段给出了一些示例。
struct S { void f(int i); };
void S::f(int i) {
[&, i]{}; // OK
[&, &i]{}; // ERROR: i preceded by & when & is the default
[=, this]{}; // ERROR: this when = is the default
[i, i]{}; // ERROR: i repeated
}
除了捕获变量,lambda 还可接受输入参数。 参数列表(在标准语法中称为 lambda 声明符)是可选的,它在大多数方面类似于函数的参数列表。例:
int y = [] (int first, int second)
{
return first + second;
};
在 C++14 中,如果参数类型是泛型,则可以使用 auto 关键字作为类型说明符。 这将告知编译器将函数调用运算符创建为模板。 参数列表中的每个 auto 实例等效于一个不同的类型参数。
auto y = [] (auto first, auto second)
{
return first + second;
};
虽然Lambda表达式的参数和普通函数的参数类似,但是还是要注意有些许区别,主要有以下几点:
在前面有说过,在Lambda表达式中,如果以传值方式,或者说值捕获的方式捕获外部变量,则函数体中不能修改该外部变量,这一点与子函数创立参数类似。否则会引发编译错误。那么有没有办法可以修改值捕获的外部变量呢?这是就需要使用mutable关键字,该关键字用以说明表达式体内的代码可以修改值捕获的变量,示例:
int main()
{
int a = 123;
auto f = [a]()mutable { cout << ++a; }; // 不会报错
cout << a << endl; // 输出:123
f(); // 输出:124
}
需要注意:被mutable修饰的lambda表达式就算没有参数也要写明参数列表。
可以使用 throw() 异常规范来指示 lambda 表达式不会引发任何异常。 与普通函数一样,如果 lambda 表达式声明 C4297 异常规范且 lambda 体引发异常,Visual C++ 编译器将生成警告 throw(),如下所示:
// throw_lambda_expression.cpp
// compile with: /W4 /EHsc
int main() // C4297 expected
{
[]() throw() { throw 5; }();
}
编译器将自动推导 lambda 表达式的返回类型。 无需使用 auto 关键字,除非指定尾随返回类型。 trailing-return-type 类似于普通方法或函数的返回类型部分。 但是,返回类型必须跟在参数列表的后面,你必须在返回类型前面包含 trailing-return-type 关键字 ->。
如果 lambda 体仅包含一个返回语句或其表达式不返回值,则可以省略 lambda 表达式的返回类型部分。 如果 lambda 体包含单个返回语句,编译器将从返回表达式的类型推导返回类型。 否则,编译器会将返回类型推导为 void。 下面的代码示例片段说明了这一原则。
auto x1 = [](int i){ return i; }; // OK: return type is int
auto x2 = []{ return{ 1, 2 }; }; // ERROR: return type is void, deducing
// return type from braced-init-list is not valid
lambda 表达式的 lambda 体(标准语法中的 compound-statement)可包含普通方法或函数的主体可包含的任何内容。 普通函数和 lambda 表达式的主体均可访问以下变量类型:
从封闭范围捕获变量,如前所述。
以下示例包含通过值显式捕获变量 n 并通过引用隐式捕获变量 m 的 lambda 表达式:
// captures_lambda_expression.cpp
// compile with: /W4 /EHsc
#include
using namespace std;
int main()
{
int m = 0;
int n = 0;
[&, n] (int a) mutable { m = ++n + a; }(4); //mutable只是一个标识符,后面的就是Lambda体。
cout << m << endl << n << endl;
}
//Output:
5
0
由于变量 n 是通过值捕获的,(变量m是通过引用捕获的),因此在调用 lambda 表达式后,变量的值仍保持 0 不变。 4是通过参数列表里的a传进。mutable 规范允许在 lambda 中修改 n。
没有捕获变量的lambda表达式可以直接转换为函数指针,而捕获变量的lambda表达式则不能转换为函数指针。原因如下:
lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终均会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。
typedef void(*Ptr)(int*);
Ptr p = [](int* p) { delete p; }; //OK
Ptr p1 = [&] (int* p) { delete p; }; //error