目录
引入
先通过一段例子来引入lambda
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
}
对于想要通过自己的意愿来进行某种比较,我们可以自己实现一个类封装仿函数来达到目的 ,但是,为了实现一个满足意愿的算法,每次都要去写这么一个类,并且每次比较的逻辑不一样,就得实现多个类,还可能会出现相同类名,这会带来一定的麻烦。
为了解决不便,c++11中便引入了lambda表达式,lambda在书写方式带来了一定的优势,类似范围for带来的感受,其实其底层的实现依然是仿函数,在底层,编译器会根据lambda表达式生成一个类,类中有对应的仿函数实现,lambda接收到的参数都会传给仿函数,那么为了进一步弄清楚,先来了解语法,学会使用,再来探究底层。
一、lambda表达式语法与使用
lambda表达式书写格式:[capture-list](parameters)mutable->return type{statement}
1.1lambda表达式各部分说明
- [capture-list]:捕捉列表,描述了上下文中哪些数据可以被lambda使用,以及使用的方式是传值还是传引用,有以下用法
①[var]:表示值的传递方式捕捉变量var
②[=]:表示值传递方式捕获所有父作用域中的所有变量(包括this)
③[&var]:表示引用传递捕捉变量var
④[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
⑤[this]:表示值传递方式捕捉当前的this指针
注意:
a.父作用域指包含lambda函数的语句块
b.语法上捕捉列表可由多个捕捉项组成,并以逗号分割
比如:[=,&a,&b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他变量
[&,a,this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c.捕捉列表不允许变量重复传递(指相同方式(传值捕获或传引用捕获)),否则就会导致编译错误
比如:[=,a]:=已经以值传递方式不捉了所有变量,再以传值捕捉a重复
d.在块作用域以外的lambda函数捕获列表必须为空
e.在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者局部变量都会导致编译报错
f.lambda表达式之间不能相互赋值,因为对于底层而言,编译器会为每个 lambda 表达式生成一个唯一的、匿名的类类型,这个类类型封装了 lambda 表达式的行为和状态(如果有捕获的变量)。
- (parameters):参数列表。相当于函数的参数列表,如果不需要参数传递,则可以省略()
- mutable:默认情况下,lambda函数的底层实现仿函数中参数列表的参数是用const修饰,lambda函数以值传递捕获到的变量都是传给仿函数的,所以这些参数都是具有常量性的,而对变量的修改其实就是对参数的修改,所以不能够直接修改,而mutable可以取消该常量性,从而可以进行修改。使用该修饰符,参数列表不可省略(即使参数为空)。注意:lambda函数即仿函数接收到的以值传过来的变量对于仿函数的参数来说是局部变量,对局部变量进行修改,并不会影响外部传过来的变量,类似函数的传参
- ->return type:返回值类型。用于声明lambda函数的返回值类型是什么,没有返回值时、明确返回值类型时可以省略,而编译器会根据返回值自己进行推导类型
- {statement}:函数体。lambda的函数体,里面是对捕获到的变量的一系列操作
lambda的语法还是比较复杂,需要不断使用加深熟练,对于lambda表达式,可以将整体理解成一个函数体对象。那么接下来根据使用来验证语法。
1.2lambda使用
cpp
class A
{
public:
void fun()
{
//捕捉父作用域即main函数所在的所有变量,以值传递方式捕捉
//由于父作用域所在区域就是fun函数,并没有a,b变量,所以捕捉的其实是this指针
//对a,b的操作其实是通过this来操作,只不过可以省略this指针不写
auto funz = [=] {cout<< a + b << endl; };
auto fun2 = [this]//直接捕捉this指针
{
cout << a * b << endl;
};
}
private:
int a = 3;
int b = 4;
};
int main()
{
//最简单的lambda表达式,但是没有任何意义
[] {};
//省略参数列表和返回值类型,返回值类型由编译器推导为int。
//其中lambda表达式是一个函数体对象,函数体中是以分号结尾,它作为一个对象赋值给另一个对象时,整个赋值是一条语句,要记得加分号
int a = 3, b = 4;
auto f1 = [a, b] {return a + b; };//捕捉多个变量a,b,以逗号分割,这些变量传给底层仿函数,仿函数中的变量是const属性,不能直接修改
cout << f1() << endl;//调用lambda即调用仿函数,由于参数列表是空的,所以不用传参
auto f2 = [=] //捕捉父作用域即main函数所在的所有变量,以值传递方式捕捉,同样也不能直接修改
{
return a - b;
};//其次,为了形象一点,函数体可以直接放下来
cout << f2() << endl;
auto f3 = [&] //捕捉父作用域即main函数所在的所有变量,以传引用方式捕捉,可以修改
{
int temp = a;
a = b;
b = temp;
cout << a << " " << b << endl;
};
f3();//由于函数体中没有返回值,编译器会识别仿函数的返回值类型是void,所以不需要打印
//省略了返回值类型,参数列表中有一个参数需要传值
auto f4 = [&a](int c) {return a+ c; };//以传引用方式捕捉变量a
cout << f4(5) << endl;//传入一个5
//捕捉this指针
A aa;
aa.fun();
auto f5 = [] {cout << "hello world" << endl; };
auto f6 = [] {cout << "heool world" << endl; };
//f5 = f6;不能相互赋值,因为对于底层而言,他们的类型不一致
cout << typeid(f5).name() << endl;
cout << typeid(f6).name() << endl;
//可以将lambda表达式赋值给相同类型的函数指针
void(*pf)();
pf = f5;
pf();//Lambda 表达式可以转换为函数指针的原因是,当 lambda 表达式没有捕获任何外部变量时,它可以隐式地转换为对应的函数指针类型。
//这使得 lambda 表达式可以像普通函数一样被调用,通过函数指针传递或赋值给函数指针变量。
}
输出结果:
其中对于lambda表达式,编译器生成了唯一的类类型,由打印结果也可知这是一个类类型,对于其作如下解释:
`int __cdecl main(void)':
- 这部分标识的是函数的返回类型和名称。在这里,它指的是函数
main
,其返回类型是int
,没有参数。
::
2'::<lambda_7> :
这部分则是标识 lambda 表达式。在C++中,lambda 表达式会被编译器转换成一个匿名的类类型,并生成一个特定的名称来标识这个 lambda 函数对象。
::
表示这个 lambda 函数对象是属于全局作用域的。
2
是编译器生成的内部编号,用于唯一标识不同的 lambda 表达式。
<lambda_7>
是 lambda 表达式的内部名称,用于区分不同的 lambda 函数对象,也由此来区分类型不一致。
对lambda的使用有了初步的了解,我们可以对最开始的商品用lambda进行改造
cpp
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](Goods& g1, Goods& g2) {return g1._price < g2._price; });//按价格升序
sort(v.begin(), v.end(), [](Goods& g1, Goods& g2) {return g1._price > g2._price; });//按价格降序
sort(v.begin(), v.end(), [](Goods& g1, Goods& g2) {return g1._evaluate < g2._evaluate; });//按评价升序
sort(v.begin(), v.end(), [](Goods& g1, Goods& g2) {return g1._evaluate > g2._evaluate; });//按评价降序
}
二、仿函数与lambda的底层调用
为了探究其底层的调用来验证上面的内容结论,可以通过反汇编的形式来观察即可,通过一段代码来演示
cpp
#include <iostream>
using namespace std;
class Rate
{
public:
Rate(double rate)
:_rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
//仿函数
double rate = 0.9;
Rate r1(rate);
cout << r1(10000, 2) << endl;
//lambda
auto r2 = [=](double money, int year)->double {return money * rate * year; };
cout << r2(10000, 2) << endl;
return 0;
}
输出结果:
反汇编:
输出结果相同,从使用方式上来说两者表达式的实现差不多,但lambda写法上更具有优势,由反汇编可知,对于仿函数来说调用的就是仿函数,而对于lambda来说,编译器会自动生成一个类,在该类中重载了operator(),最终也是调用了该仿函数。
end~