泛型算法中的定制操作
很多算法都会比较输入序列中的元素,通过定制比较动作,可以控制算法按照编程者的意图工作。比如string类型中缺省排序如下:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<string> v{"This","is", "a", "predicate", ".", "b", "c"};
sort(v.begin(), v.end());
for(const auto& s:v){
cout << s << "\t";
}
return 0;
}
代码的输出如下:
缺省动作是按照字母顺序排序的
谓词
假设有一个需求,希望按照字符串长度从小到大排序。可以先定义一个比较函数
bool compare(const string& s1, const string& s2){
return s1.size() < s2.size();
}
然后将这个函数传递给sort算法即可
sort(v.begin(), v.end(), compare);
for(const auto& s:v){
cout << s << "\t";
}
代码的输出如下:
这种作为参数传递给sort算法的函数可以看作一个动作,它有一个名称:谓词
lambda表达式
前面的例子中,定义了一个函数传递给sort算法。这个函数可以重复使用还好,如果只是使用一次就比较麻烦。这种情况下可以使用C++22提供的新特性:lamba表达式:
sort(v.begin(), v.end(),
[](const string& s1, const string& s2){
return s1.size() > s2.size();
});
for(auto s:v){
cout << s << "\t";
}
和使用谓词的情况比较可以看到:
-
没有定义函数(没有函数名)。
-
依然定义了动作,参数。
这种没有定义函数的指定动作(谓词)的方式就是lamba表达式
lamba表达式是可调用对象
可调用对象
对于一个表达式e,如果可以编写代码以e(args)的形式执行它,就可以说e是可调用的。
例如下面的函数:
int add(int a, int b);
可以这样编写代码:
int ret = add(1, 2);
按照前面提到的原则,就可以说add是可调用的。到C++98为止,可调用对象有函数,函数指针,函数对象等。
lamda表达式
C++11之后引入的lambda表达式也是一种可调用对象,其标准形式如下:
[capture list] (parameter list) mutable -> return type {function body}
- capture list,称作捕捉列表。捕捉列表总是出现在lambda函数的开始处。事实上,[]是lambda的引出符。编译器根据该引出符判断接下来的代码是否是lamda函数。捕捉列表能够捕捉上下文中的变量以供lambda函数使用
- parameter list:表示参数列表,和普通函数一样。如果不需要参数传递,可以和()一起省略
- mutable :默认情况下,lambda函数是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)
- ->returne type:普通函数一样,表示返回值类型。不需要返回值的时候,可以连同->一起省略。在返回值明确的情况下,让编译器对返回类型进行推导
- {function body}:函数体。内容与普通函数一样。不过除了可以使用参数之外,还可以使用所有捕获的变量
int main(){
[]{}; // 最简单的lamda函数
int a = 3;
int b = 4;
[=] {return a + b;} // 省略了参数列表和返回类型,返回类型由编译器推导为int
auto func() = [&](int c) {b = a + c;} //省略了返回类型,无返回值
auto func2() = [=, &b](int c) ->int{return b = a + c2}; // 各部分都很完整的lambda函数
}
例如可以这样使用lambda表达式:
auto add = [](int a, int b){return a + b;};
cout << add(1, 2) << endl;
可以把lambda表达式通过变量传递,像函数一样调用。当然也可以这样:
int result = [](int a, int b){return a*b;}(2, 3);
cout << result << endl;
可以看到[](int a, int b){return a*b;}这个表达式可以以e{args}的方式被调用,也就是说,这是一个可调用对象。
捕捉列表
只管的讲,lamdba函数与普通函数可见的最大区别之一,就是lambda函数可以通过捕捉列表访问上下文中的数据。具体的,捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用方式(以值传递或者引用传递)。
int main(){
int boys = 4, girls = 3;
auto totalChild = [girls, &bodys]() -> int{ return girls + boys; };
return totalChild ;
}
上面的boys和girls可以视为lambda函数的一种初始状态,lambda函数的运算则是基于初始状态进行的计算。这与函数简单基于参数的运行是不一样的。
语法上,捕捉列表由多个捕捉项组成,以,分割。捕捉列表有如下几种形式:
- [var]表示值传递方式捕捉变量var
- [=]表示值传递方式捕捉所有父作用域的变量(包括this)
- [&var]表示引用传递捕捉变量var
- [&]表示引用传递捕捉所有父作用域的变量(包括this)
- [this]表示以值传递方式捕捉当前this指针
父作用域:这里指的是包含lamda函数的语句块
通过一些组合,捕获列表可以表达式更复杂的意思,比如:
- [=, &a, &b]表示以引用方式捕捉变量a和b,值传递方式捕捉其他所有变量
- [&, a, this]表示以值传递方式捕捉变量a和this,引用传递方式捕捉其他所有变量
不过,值得注意的是,捕捉列表不允许重复传递,否则会导致编译器错误,比如:
- [=, a]:这里=已经以值传递方式捕捉了所有变量,捕捉a重复
- [&, &this]:这里已经以引用方式捕捉了所有变量,再捕捉this也是一种重复
int main(){
int boys = 4, girls = 3;
auto totalChild = [=]()->int{return boys + girls}; // =捕捉所有父作用域的变量
return totalChild
}
函数对象
考虑下面的代码
bool istarget(const string& s){
return s.size() < 2;
}
vector<string> v{"This","is", "a", "predicate", "."};
auto found = find_if(v.begin(), v.end(), istarget);
cout << *found << endl;
使用find_if算法从给定的vector中找到第一个长度小于2的对象
如果我们希望在istarget中选择string时使用变量而不是固定的2的时候,一般的函数就不能满足需求了(虽然使用全局变量算是一个选项)。这时的一个选择就是函数对象。
实现定义一个重载()运算符的IsTarget类
class IsTarget{
unsigned int max;
public:
IsTarget(int m_max):max(m_max){}
bool operator()(const string s){
return s.size() < max;
}
};
IsTarget类可以像下面这样使用:
string test("H");
IsTarget it(2);
cout << it(test) << endl;
注意it(test)的部分,从代码的形式来看,这个类的对象可以以e(args)的形式编码并执行,因此它也是可执行对象。长度信息是在创建对象it时指定,在()运算符被执行是使用。在回到find_if的例子,可以这样使用IsTarget类:
vector<string> v{"This","is", "a", "predicate", "."};
auto found = find_if(v.begin(), v.end(), IsTarget(2));
cout << *found << endl;
在执行find_if时,比较的长度信息作为参数传递给IsTarget类,以便在执行选择操作时使用。
使用捕获列表
使用函数对象还是有些麻烦,继续请出lambda表达式,它有一个被[ ]包围的捕获列表,用于捕获lambda表达式所在函数的局部变量:
vector<string> v{"This","is", "a", "predicate", "."};
unsigned int max = 2;
auto found = find_if(v.begin(), v.end(),
[max](const string& s){return s.size() < max;});
max是在find_if执行的函数里定义的变量,将其包含在lambda表达式的捕获列表[ ]中以后,就可以在lambda表达式中使用它了。
值捕获
先看如下代码:
int factor = 2;
auto multiply = [factor](int value)
{return factor * value;};
factor = 4;
cout << multiply(2) << endl;
代码中首先为factor赋值2,创建lambda表达式以后,再次赋值4。由于lambda表达式的捕获是在该表达式创建是进行的,而第二次赋值在lambda表达式创建之后,所以muliply(2)的执行结果为4。
引用捕获
还是这段代码,只要在捕获列表中变量的前面多了一个&,就变成了引用捕获。
int factor = 2;
auto multiply = [&factor](int value)
{return factor * value;};
factor = 4;
cout << multiply(2) << endl;
捕获的时机并没有变化,只是捕获的是factor的引用而不是factor的值,所以定义lambda之后,对factor再次赋值4依然会影响multiply(2)的结果。此时的输出为8。
前面例子中使用捕获列表时,具体指定了变量名,属于显示捕获。另外还有隐式捕获,由lambda表达式推断需要捕获的变量。具体方法是:
- 当需要隐式值捕获时,使用[=];
- 当需要隐式引用捕获时,使用[&];
可变lambda
假设有如下vector,保存的内容是学生的考试成绩:
vector<int> score{45, 70, 56, 86, 28, 60, 90};
可以用以下代码来寻找第一个及格成绩:
find_if(score.begin(), score.end(),
[](int v){return (v >=60);});
如果需要找到第n个及格成绩,很自然地会考虑使用下面的代码:
int counter = 2;
find_if(score.begin(), score.end(),
[counter](int v){
return (v >=60)&&(--counter == 0);
});
但 是很遗憾,这时会出现编译错误,告诉你counter是只读的。其原因是因为在lambda表达式中很少有需要修改捕获值的场景,因此默认捕获值具有 const属性。如果出现本例这样,确实希望修改捕获值的情况,C++11使用mutable关键字来解决这个问题。来看完整代码:
int counter = 2;
auto iter find_if(score.begin(), score.end(),
[counter](int v)mutable{
return (v >=60)&&(--counter == 0);
});
cout << *iter << endl;
当然了,由于是值捕获,处于lambda表达式外面的counter值依然不会改变。
如果希望连外面的counter一起修改,使用引用捕获即可。
lamba表达式
为什么是lambda?
首先这个lambda就是罗马字母λ,lambda表达式即λ表达式。数学上有一个概念叫λ演算,其中的一个内容就是λ表达式。
考虑普通的数学函数表示方法:
f(x) = 2x + 1
按照λ表达式的规则,可以写成:
λx.(2x+1)
这个表达式可以读成“对于参数x,2x+1。这里的Lambda,λ,仅仅表达的是数学中"函数"的概念。
各种编程语言,也引入了λ(lambda)表达式。例如:
C#语言:(x) =>{ return 2x+1; }
Java语言:(x) ->{ return 2x+1; }
C++11中也同样引入了lambada表达式,
[](int x)->int{ return 2 * x + 1;}
对于程序员来讲,lambda表达式提供了一种实现无名函数的方法。
无名的烦恼
lambda表达式不需要定义函数(名),在大多数场景下,这是一种便利,但也会带来一些烦恼,例如递归调用。因为没有函数名,如何调用自己就成了一个问题。
当然了,这个问题是可以解决的。这里以阶乘为例进行说明,直接上代码:
function<int(int)> factorial =
[&](int n){
if(n < 2) return 1;
return n * factorial(n - 1);
};
cout << factorial(3) << endl;
lambda表达式的递归调用有几个要点:
-
使用标准库中的function模版类型定义表达式类型,其中模范参数与lambda表达式的返回值,参数一致。
-
使用引用捕获来获得factorial的使用权。
-
调用factorial实现递归调用。
总结:可以认为lambda表达式取得信息有两种方式,或者说两个时机:一个是参数列表,其内容是在表达式被调用时决定;另一个捕获列表,其内容是在是表达式被创建的时候决定