C++11 泛型算法之Lambda表达式
1 什么是Lambda表达式?
首先,我们理解一下函数,函数是什么?函数就是一个命名了的代码块,我们通过调用函数来执行相应的代码。那什么是Lambda 表达式呢?Lambda 表达式(lambda expression)也被称为匿名函数,即没有函数名的函数。
与函数类似,一个lambda有一个返回类型、一个参数列表和一个函数体;与函数不同,lambda可能定义在函数内部,且必须使用尾置返回来指定返回类型(尾置返回看不懂没关系,后面会说)。
2 怎么写Lambda表达式?
一个lambda表达式具有如下形式:
[capture list] (parameter list) -> return type { function body }
- capture list :捕获列表
- parameter list :参数列表
- return type :返回类型
- function body :函数体
当然,我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体。
auto f = [] { return 42; }
Lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符( () )。
cout << f() << endl;
2.1 捕获列表
首先,我们需要知道捕获列表有什么用呢?当一个lambda表达式出现在一个函数中, 如果需要使用函数中的局部变量(非static变量),需要将局部变量包含在其捕获列表中。如下例所示。
// 获取一个迭代器,指向第一个满足size()>=6的元素 auto A() { int sz = 6; static int static_sz = 6; vector<string> words = {"apple","banana","orange"}; // 正确:sz已捕获 auto we = find_if(words.begin(), words.end(), [sz] (const string &a) { return a.size()>= sz; }); // 错误:sz未捕获 auto we1 = find_if(words.begin(), words.end(), [] (const string &a) { return a.size()>= sz; }); // 错误:无法在lambda中捕获带有静态储存持续时间的变量 auto we2 = find_if(words.begin(), words.end(), [static_sz ] (const string &a) { return a.size()>= static_sz ; }); return we; }
那么,我们应该怎么写捕获列表,捕获列表有哪些表现形式,分别表示什么含义呢?下表展示了捕获列表的各种形式及其对应的含义。
| 空捕获 | [] | 空捕获列表。lambda不能使用所在函数中的变量。 |
| 显式捕获:值捕获 | [A,B,...] | A,B等名字都是lambda所在函数的局部变量。该形式下,捕获列表中的变量都被拷贝,属于值捕获。 |
| 显式捕获:引用捕获 | [&A,&B,...] | A,B等名字都是lambda所在函数的局部变量。该形式下,采用的是引用捕获方式。 |
| 隐式捕获:引用捕获 | [&] | 隐式捕获列表,采用引用捕获方式。lambda体中所使用的来自所在函数的实体都采用引用方式使用 |
| 隐式捕获:值捕获 | [=] | 隐式捕获列表,采用值捕获方式。lambda体将拷贝所使用的来自所在函数的实体的值 |
| 混合使用:隐式引用捕获和显式值捕获 | [&,identifier_list] | identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list中的名字前而不能使用& |
| 混合使用:隐式值捕获和显式引用捕获 | [=,identifier_list] | identifier_list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括this,且这些名字之前必须使用& |
2.1.1 空捕获
空捕获是指lambda表达式的捕获列表为空,即未捕获任何变量。如下例所示,lambda表达式f的函数体内部只有一条return语句,不需要用到fcn1函数中的任何变量,所以捕获列表为空。
void fcn1() { // 捕获列表为空 auto f = [] { return 42; } cout << f() << endl; }
2.1.2 显示捕获
(1)值捕获
与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在Lambda创建时拷贝,而不是调用时拷贝:
void fcn2() { size t v1 = 42; // 局部变量 // 值捕获:将v1拷贝到名为f的可调用对象 auto f = [v1] { return vl; }; v1= 0; auto j = f () ; // j为42;f保存了我们创建它时v1的拷贝 }
(2)引用捕获
void fcn3() { size t v1 = 42; // 局部变量 // 对象f2包含v1的引用 auto f2 = [&v1] { return v1; }; v1 = 0; auto j = f2 () ; // j为0;f2保存v1的引用,而非拷贝 }
引用捕获与返回引用有着相同的问题与限制。在以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量就己经消失了。
void biggies(vector<string> &words,vector<string>::size_type sz, ostream &os = cout, char c = ' ') { // 依次打印到os 调用for_each时,Lambda会立即执行。在此情况下,以引用方式捕获os没有问题 for_each(words.begin(), words.end(), [&os, c](const string &s) ( os << s << c;}); }
2.1.3 隐式捕获
捕获除了上述显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda函数体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个"&"或"="。"&"告诉编译器采用捕获引用方式,而"="表示采用值捕获方式。以下示例,用隐式捕获重写上述显示捕获示例。
(1)值捕获
void fcn2() { size t v1 = 42; // 局部变量 // 隐式捕获:值捕获 auto f = [=] { return v1; }; v1= 0; auto j = f () ; // j为42;f保存了我们创建它时v1的拷贝 }
(2)引用捕获
void fcn3() { size t v1 = 42; // 局部变量 // 对象f2包含v1的引用 auto f2 = [&] { return v1; }; v1 = 0; auto j = f2 () ; // j为0 }
2.1.4 混合使用显示捕获与隐式捕获
如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获, 可以混合使用隐式捕获和显式捕获。当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个"&"或"="。
当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。 即,如果隐式捕获是引用方式(使用了"&"), 则显式捕获命名变量必须采用值方式, 因此不能在其名字前使用"&"。类似的, 如果隐式捕获采用的是值方式(使用了"="), 则显式捕获命名变量必须采用引用方式, 即,在名字前使用"&"。
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 string &s) { os << s << c; }) ; // os显式捕获,引用捕获方式;c隐式捕获,值捕获方式 for_each(words.begin(), words.end(), [=,&os] (const string &s) { os << s << c; }) ; }
2.2 参数列表
lambda不能有默认参数。因此,一个lambda表达式调用的实参数目永远与形参数目相等。
// 带有参数列表的Lambda表达式 [] (const string &a, const string &b) {return a.size() < b.size();}
2.3 返回类型
2.3.1 忽略返回类型
如果像如下代码所示,忽略lambda表达式的返回类型,即省略了"-> return type"。lambda表达式可以根据函数体中的代码推断出返回类型。前提是函数体只是一个return语句,返回类型从返回表达式的类型推断而来。
// 可推断返回类型为int transform(vi.begin(),vi.end(),vi.begin(), [] (int i) {return i < 0 ? -i : i;});
如果lambda的函数体包含任何单一return语句以外的内容,且未指定返回类型,则默认返回void。
// 错误:编译器推断这个版本的lambda返回类型为void,但它却返回了一个int transform(vi.begin(),vi.end(),vi.begin(), [] (int i) {if(i<0) return -i; else return i;});
2.3.1 定义返回类型(尾置返回)
当我们需要为一个lambda表达式定义返回类型时,必须使用尾置返回类型。尾置返回类型,顾名思义,就是将返回类型放置在后面。跟在参数列表之后,以"->"加上返回类型来表示。
// 正确:指定了返回类型为int transform(vi.begin(),vi.end(),vi.begin(), [] (int i) -> int {if(i<0) return -i; else return i;});
2.4 函数体
函数体,即是我们需要lambda表达式执行的代码块。也就是lambda表达式"{ }"中的内容。
2.4.1 函数体中可以使用的变量
在lambda函数体中,除了可以使用我们之前说过的捕获列表中的变量,还可以直接使用static变量和它所在函数之外声明的名字。
int out_sz = 6; auto A() { static int static_sz = 6; vector<string> words = { "apple","banana","orange" }; // 正确:Lambda可以直接使用它所在函数之外声明的变量 auto we = find_if(words.begin(), words.end(), [](const string &a) { return a.size() >= out_sz; }); // 正确:Lambda可以直接使用局部static变量 auto we1 = find_if(words.begin(), words.end(), [](const string &a) { return a.size() >= static_sz; }); // 错误:无法再lambda中捕获带有静态储存持续时间的变量 auto we2 = find_if(words.begin(), words.end(), [static_sz](const string &a) { return a.size() >= static_sz; }); auto we3 = find_if(words.begin(), words.end(), [out_sz](const string &a) { return a.size() >= out_sz; }); return we; }
2.4.1 函数体中可以使用的函数
以下例子在函数体中使用了名字cout,cout不是定义在函数B中的局部名字,而是定义在头文件<iostream>中。因此,只要在B出现的作用域中包含了头文件<iostream>,我们的lambda就可以使用cout。
void B() { vector<string> words = { "apple","banana","orange" }; // 打印单词,每个单词后面接一个空格 for_each(wc, words.end(), [] (const string &s) { cout << s << " "; }); cout << endl; }
2.4 可变lambda
上面我们已经知道了值捕获时,捕获变量都为拷贝,无法修改原变量的值。如果我们希望能改变一个被捕获的变量的值,可以在参数列表后加上关键字mutable。
void fcn3() { size_t v1 = 42; // f可以改变它所捕获的变量的值; auto f = [v1] () mutable {return ++v1;}; v1 = 0; auto j = f(); // j为43 }
这里要注意,不要跟引用捕获混淆哟!将上述fcn3修改为引用捕获,感受一下不同之处。
void fcn3() { size_t v1 = 42; // f可以改变它所捕获的变量的值; auto f = [&v1] () {return ++v1;}; v1 = 0; auto j = f(); // j为0 }
无论是通过值捕获去修改,还是通过引用捕获去修改,都要必须要捕获变量是可修改左值。如果上述fcn3中的v1变量是const,则不允许修改。
3 什么时候用lambda表达式?
对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的。如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好。
如果lambda表达式为空,我们通常可以用函数来替代它。如修改以下lambda示例:
// 按长度排序,长度相同的单词维持字典序 stable_sort (words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.size(); });
改写为函数形式:
// 比较函数,用来按长度排序单词 bool isShorter(const string &s1, const string &s2) { return s1.size() < s2.size(); } // 按长度由短至长排序words sort(words begin(), words.end(), isShorter);
但是,对于捕获局部变量的lambda,用函数来替换它就不是那么容易了。例如以下示例:
// 获取一个迭代器,指向第一个满足size()>=6的元素 auto A() { int sz = 6; vector<string> words = {"apple","banana","orange"}; // sz已捕获 auto we = find_if(words.begin(), words.end(), [sz] (const string &a) { return a.size()>= sz; }); return we; }
我们可以很容易将find_if函数中的lambda表达式,改写为一个完成同样工作的函数:
bool check_size(const string &s, string::size_type sz) { return s.size()>= sz; }
但是,我们不能用这个函数作为find_if的第三个参数。因为find_if函数的第三参数仅接受一元谓词,因此传递给find_if的可调用对象仅接受单一参数。为了用check_size 来代替此lambda,必须解决如何传递参数sz的问题。
浙公网安备 33010602011771号