一个lambda表达式用于创建闭包。lambda表达式与任何函数类似,具有返回类型、参数列表和函数体。与函数不同的是,lambda能定义在函数内部。lambda表达式具有如下形式
[ capture list ] ( parameter list) -> return type { function body }
capture list,捕获列表,是一个lambda所在函数中定义的局部变量的列表。lambda函数体中可以使用这些局部变量。捕获可以分为按值捕获和按引用捕获。非局部变量,如静态变量、全局变量等可以不经捕获,直接使用;
parameter list,参数列表。从C++14开始,支持默认参数,并且参数列表中如果使用auto的话,该lambda称为泛化lambda(generic lambda);
return type,返回类型,这里使用了返回值类型尾序语法(trailing return type synax)。可以省略,这种情况下根据lambda函数体中的return语句推断出返回类型,就像普通函数使用decltype(auto)推导返回值类型一样;如果函数体中没有return,则返回类型为void。
function body,与任何普通函数一样,表示函数体。
Lambda表达式可以忽略参数列表和返回类型,但必须包含捕获列表和函数体:
auto f = [] { return 42; } cout << f() << endl;
上面的lambda表达式,定义了一个可调用对象f,它不接受参数,返回42。Lambda的调用方式与普通函数的调用方式相同。
lambda表达式是用于生成闭包的纯右值(prvalue)表达式。每一个lambda表达式都定义了独一无二的闭包类,闭包类内主要的成员有operator()成员函数:
ret operator()(params) const { body } //the keyword mutable was not used ret operator()(params) { body } //the keyword mutable was used template<template-params> //since C++14, generic lambda ret operator()(params) const { body } template<template-params> //since C++14, generic lambda, the keyword mutable was used ret operator()(params) { body }
当调用lambda表达式生成的闭包时,执行operator()函数。除非lambda表达式中使用了mutable关键字,否则lambda生成的闭包类的operator()函数具有const饰词,从而lambda函数体中不能修改其按值捕获的变量;如果lambda表达式的参数列表中使用了auto,则相应的参数称为模板成员函数operator()的模板形参,该lambda表达式也就成了泛化lambda表达式。
如果捕获列表中,有按值捕获的局部变量,则闭包类中就会有相应的未命名成员变量副本,这些成员变量在定义lambda表达式时就由那些相应的局部变量进行初始化。如果按值捕获的变量是个函数引用,则相应的成员变量是引用指向函数的左值引用;如果是个对象引用,则相应的成员变量是该引用指向的对象。如果是按引用捕获,标准中未指明是否会在闭包类中引入相应的成员变量。
该闭包类还有其他成员函数。比如转换为函数指针的转换函数、构造函数(包括复制构造函数)、析构函数等,具体可参考https://en.cppreference.com/w/cpp/language/lambda
一:捕获列表
lambda可以定义在函数内部,使用其局部变量,但它只能使用那些明确指明的变量。lambda通过将外部函数的局部变量包含在其捕获列表中来指出将会使用这些变量。
当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类。当向函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是编译器生成的类类型的未命名对象;类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。
默认情况下,从lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员在lambda对象创建时被初始化。
1:值捕获
类似参数传递,变量的捕获方式可以是值或引用。与传值参数类似,采用值捕获的前提是变量可以拷贝。被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝:
int v1 = 42; auto f=[v1]{return v1;}; v1=0; auto j = f(); //j is 42
由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值。
2:引用捕获
定义lambda时可以采用引用方式捕获变量。例如:
int v1 = 42; auto f=[&v1]{return v1;}; v1=0; auto j = f(); //j is 0
v1之前的&指出v1应该以引用方式捕获。一个以引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象。在本例中,当lambda返回v1时,它返回的是v1指向的对象的值。
引用捕获与返回引用有着相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失,这就是未定义行为。
引用捕获有时是必要的:
void biggies(vector<string> &words, vector<string>::size_ type sz, ostream &os=cout, char c=' ') { for_each(words.begin(), words.end(), [&os, c](const strinq &s) { os << s << c; }); }
不能拷贝ostream对象,因此捕获os的唯一方法就是捕获其引用。当我们向一个函数传递lambda时,就像本例子调用for_each那样,lambda会在函数内部执行。在此情况下,以引用方式捕获os没有问题,因为当for_each执行时,biggies中的变量是存在的。
我们也可以从一个函数返回lambda。函数可以直接返问一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获。
3:隐式捕获
除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个” &” 或”=”。 ” &”告诉编译器采用引用捕获方式,”=”则表示采用值捕获方式。例如:
we = find_if(words.begin(), words.end(), [=](const string &s) { return s.size() >= sz; });
如果希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获:
void biggies(vector<string> &words, vector<string>::size_ type sz, ostream &os=cout, char c=' ') { //os隐式捕获,引用捕获方式;c显式捕获,值捕获方式 for_each(words.begin(), words.end(), [&, c](const strinq &s) { os << s << c; }); //os显式捕获,引用捕获方式;c隐式捕获,值捕获方式 for_each(words.begin(), words.end(), [=, &os](const strinq &s) { os << s << c; }); }
当混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个”&”或”=“。此符号指定了默认捕获方式为引用或值;并且显式捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式,则显式捕获命名变量必须采用值方式;类似的,如果隐式捕获采用的是值方式,则显式捕获命名变量必须采用引用方式。
二:可变lambda
默认情况下,对于一个按值捕获的变量,lambda不能改变其值。如果希望能改变这个被捕获的变量的值,就必须在参数列表之后加上关键字mutable,因此,可变lambda不能省略参数列表:
int v1 = 42; auto f=[v1] () mutable {return ++v1;}; v1=0; auto j = f(); //j is 43
一个引用捕获的变量是否可以修改依赖于此引用指向的是一个const类型还是一个非const类型:
int v1 = 42; auto f=[&v1] () {return ++v1;}; v1=0; auto j = f(); //j is 1