引入:
首先来看一个例子
struct fruit
{
double _price;
int _evalute;
string _name;
fruit(const char* str, int a, double price)
:_name(str)
,_evalute(a)
,_price(price)
{
}
};
struct ComparePriceGreater
{
bool operator()(const fruit& g1, const fruit& gr)
{
return g1._price > gr._price;
}
};
struct ComparePriceLess
{
bool operator()(const fruit& g1, const fruit& gr)
{
return g1._price < gr._price;
}
};
int main()
{
vector<fruit> v = { {"苹果",3,2.5},{"香蕉",2,3.5},{"梨子",5,5.5} };
sort(v.begin(), v.end(), ComparePriceGreater());//不知道怎么去比较,所以我们要传入一个仿函数
sort(v.begin(), v.end(), ComparePriceLess());
return 0;
}
如图,我们知道,std::sort函数在排序时,如果是内置类型,就默认是升序排列,但是如果是自定义类型呢?在lambda表达式引入之前,我们通常是写一个仿函数,然后重载(),从而达到排序的效果。
但是,每次为了实现这样一个算法,就都要去重新写出一个类。特别是相同类的命名问题,都给我们带来了很大的不方便。
lambda表达式是在C++11引入的语法,一般用于定义匿名函数,使代码更加灵活方便。
lambda表达式的格式
[capture-list] (parameters) mutable -> return-type { statement}
各个部分的说明
-
capture-list:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量提供给lambda表达式使用。
-
parameters:参数列表,与普通的函数很是相似,如果不需要传递参数,直接省略即可,甚至可以连()一起省略。
-
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表括号不能省略。
-
returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。前边的->不可以忽略。
-
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。
如上边所说,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
捕捉列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)。
[this]:表示值传递方式捕捉当前的this指针,上述例子中=改为this,大家可以尝试一下。
注意事项
a. 父作用域指包含lambda函数的语句块,即包含该lambda表达式的大括号中。
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
当然可以不同类型的捕捉。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复。
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同。
使用lambda表达式改进上边的代码
struct fruit
{
double _price;
int _evalute;
string _name;
fruit(const char* str, int a, double price)
:_name(str)
,_evalute(a)
,_price(price)
{
}
};
int main()
{
vector<fruit> v = { {"苹果",3,2.5},{"香蕉",2,3.5},{"梨子",5,5.5} };
sort(v.begin(), v.end(), [](const fruit& f1, const fruit& f2) {return f1._price > f2._price; });
sort(v.begin(), v.end(), [](const fruit& f1, const fruit& f2) {return f1._price < f2._price; });
return 0;
}
通过上述代码,我们可以很清晰的看出代码更加简洁,没有那么冗余了。
仿函数和lambda表达式
仿函数就是重载()运算符的对象,lambda表达式和仿函数功能很是相似,都可以作为仿函数表达式进行传递,那么他们的底层都是如何运转的呢?
我们来细细探究探究
其实我们可以将lambda当做一个匿名函数对象
转到反汇编来观察是如何实现的。
可以看出,lambda实际上会作为一个匿名函数对象进行传递,函数名省略,函数参数和函数体和仿函数相同。就像范围for一样,看着感觉会很复杂,其实底层还是使用迭代器进行遍历,一切为了方便而已。
我们还可以利用typeid来查看函数类型。
编译器是如何看lambda呢?其实就像仿函数一样。
class LambdaClass
{
public:
int operator () (int a, int b) const
{
return a + b;
}
};
LambdaClass plus;
int c = plus(1, 2);
lambda表达式会把主要的部分提取出来,让我们的代码编写更加简便。
那么捕获列表中捕获的变量呢?其实就是作为类的成员变量,如果是值拷贝,那么我们重载的()函数就是const函数的,是无法修改捕获的值的。
class LambdaClass
{
public:
LambdaClass(int xx, int yy)
: x(xx), y(yy) {}
int operator () (int a, int b) const
{
return x + y + a + b;
}
private:
int x;
int y;
}
int x = 1; int y = 2;
LambdaClass plus(x, y);
int c = plus(1, 2);
如果我们想要修改捕获的值呢?如改变上边的x,只需要加上关键字mutable。
int x = 1; int y = 2;
auto plus = [=] (int a, int b) mutable -> int { x++; return x + y + a + b; };
int c = plus(1, 2);
如果是值引用呢?
我们只需要进行引用捕获即可。
class LambdaClass
{
public:
LambdaClass(int& xx, int& yy)
: x(xx), y(yy) {}
int operator () (int a, int b)
{
x++;
return x + y + a + b;
}
private:
int &x;
int &y;
};
此时在函数中改变传入的值就可以改变捕获的值。
对应仿函数,我们可以最后再把lambda表达式和仿函数类的各个成分对引起来就是如下关系。
捕获列表,对应LambdaClass类的private成员。
参数列表,对应LambdaClass类的成员函数的operator()的形参列表
mutable,对应 LambdaClass类成员函数 operator() 的const属性 ,但是只有在捕获列表捕获的参数不含有引用捕获的情况下才会生效,因为捕获列表只要包含引用捕获,那operator()函数就一定是非const函数。
返回类型,对应 LambdaClass类成员函数 operator() 的返回类型
函数体,对应 LambdaClass类成员函数 operator() 的函数体。
注意:引用捕获和值捕获不同的一点就是,对应的成员是否为引用类型。
本文结束,如果有问题还请及时提出,我会虚心改正,感谢大家的观看。