1.C++中的可调用对象
在学习lambda表达式之前,咱们先来盘点一下C++中的那些可调用对象。
C++中的可调用对象有哪些?如下所示:
- 函数指针 ------ 类型复杂,不方便使用
- 仿函数对象 ------ 类型不同,不能复用代码
- lambda表达式 ------ 语法层没有类型,使用方便
为什么要有这么多种的可调用对象呢? 举个例子:可调用对象的发展史就好比手机的发展史;座机->按键手机->智能手机,他们都具有打电话的功能,为什么要不断地完善发展呢?说白了,就是为了方便,为了满足当今生活的需求。(博主我曾经向换回按键手机,发现根本做不到,现如今的手机和生活早已高度绑定)编程语言中特性的发展也是如此,在编程语言的不断使用和发展中,总会产生这样或那样的新需求,有了新需求,就要有新的解决措施,不然,就成历史遗留问题了。
2.函数指针
变量指针指向一个变量,数组指针指向一个数组,那,函数指针就是指向一个函数的漏喽
声明函数指针的格式:返回类型 (*指针名称)(参数类型列表);
使用举例:使用函数指针调用add函数,完成两个整数的相加。
代码如下:
#include <iostream>
using namespace std;
int add(int a, int b)
{
return a+b;
}
int main()
{
// 声明函数指针
int (*ptr)(int,int);
// 初始化函数指针
ptr = add;
// 通过函数指针调用add函数
int ret = ptr(1,2);
cout << ret;
return 0;
}
你可能会说,函数指针挺好用的呀,在上述例子中确实是这样,但是,如果函数稍微复杂一点,使用场景复杂一点,那就是另一个故事了~ 更何况,函数指针的声明较为复杂,不方便使用,容易写错。于是,在C++的STL库中添加了仿函数。
3.仿函数
什么是仿函数呢?仿函数又称函数对象,就是可以像函数一样使用的对象。
如何做到的呢?在该类中重载了operator(),使得该类的对象可以像函数一样使用。
举个例子:我们有一个自定义类型Date类,使用仿函数的方式比较Date类对象是否相等;
代码如下:
#include <iostream>
using namespace std;
struct Date
{
Date(int year,int month,int day)
:_year(year)
,_month(month)
,_day(day)
{}
int _year;
int _month;
int _day;
};
struct cmp
{
bool operator()(Date& d1, Date& d2)
{
if(d1._year == d2._year && d1._month == d2._month && d1._day == d2._day)
return true;
return false;
}
};
int main()
{
Date d1(2024,8,29);
Date d2(2020,10,30);
cmp c;
if(c(d1,d2))
cout << "相等";
else
cout << "不相等";
return 0;
}
仿函数更加符合面向对象的编程思想,使用上也比函数指针更简单,但还是存在不足之处。比如:我们想要比较一个自定义类型的数据,假设这个自定义类型的手机类型把,我们可以按照其价格比较,也可以按照手机的内存大小比较,也可以根据用户的评价比较......比较的需求会有很多,此时,我们应该如何写代码呢?
按照仿函数的使用方式来看,如果想要使用仿函数来进行比较,我们就需要定义多个类,在每个类中重载operator(),每个operator() 中按照不同的逻辑比较。(比较逻辑有很多,而一个类中只能重载一个 () ,所以需要多个类)
代码如下:
struct cmpWithPrice
{
bool operator()(Phone& p1,Phone& p2)
{
return p1.price > p2.price;
}
};
struct cmpWithComment
{
bool operator()(Phone& p1,Phone& p2)
{
return p1.Comment > p2.Comment;
}
};
// ...
使用仿函数的话,每当我们有新的比较需求,都需要实现一个类,成本太大。于是lambda表达式应运而生。
4.lambda表达式
终于降到 lambda 表达式了~
lambda表达式的书写格式
格式:[捕捉列表] (参数) mutable -> 返回类型 { 函数体 }
(额,怎么感觉lambda表达式 "相貌丑陋",好像挺难的?非也非也!lambda表达式并不难。)
lambda表达式使用示例代码:
int main()
{
vector<fruits> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& f1, const Goods& f2){
return f1._price < f2._price; });
sort(v.begin(), v.end(), [](const Goods& f1, const Goods& f2){
return f1._price > f2._price; });
sort(v.begin(), v.end(), [](const Goods& f1, const Goods& f2){
return f1._evaluate < f2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& f1, const Goods& f2){
return f1._evaluate > f2._evaluate; });
}
可以看出lambda表达式能够代替函数对象作为参数使用。
lambda表达式各部分说明
- [捕捉列表] : 该列表总是出现在lambda函数的开始位置,编译器根据 [] 来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (参数列表 ): 与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。
- mutable: 默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性 。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype: 返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回
值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推
导。返回值类型可写可不写。 - {statement}: 函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。
捕捉列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递 方式捕捉变量var(var是变量名)
- [=]:表示值传递 方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递 方式捕捉变量var
- [&]:表示引用传递 方式捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
使用lambda表达式的注意事项
- 1.在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。( 父作用域指包含lambda函数的语句块**)。**
- 2. 在块作用域以外的lambda函数捕捉列表必须为空。
- **3.捕捉列表不允许变量重复传递,否则就会导致编译错误。**比如:[=, a],=已经以值传递方式捕捉了所有变量,捕捉a重复。
- 4. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。 比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
- 5. lambda表达式之间不能相互赋值,即使看起来类型相同。
lambda表达式的底层原理
lambda表达式到底是什么 ?从上面lambda表达式的使用示例代码可以看出,lambda表达式其实就是一个局部的匿名函数。 说白了lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
示例代码:
int main()
{
auto f = []{cout << "hello world" << endl; };
f();
return 0;
}
函数对象和lambda表达式的使用对比
看一段代码:
struct add
{
public:
int operator()(int a, int b)
{
return a+b;
}
};
int main()
{
// 函数对象
add a1;
a1(1, 2);
// lambda
auto a2 = [=](int a, int b)->int{return a+b;}
a2(2, 2);
return 0;
};
上面这段代码,main函数中的汇编如下:
可以看到 仿函数对象 和 lambda表达式 都调用operator()了。嗯,等等,仿函数对象调用operator()我懂,但是lambda表达式怎么也会调用operator() 呢?它的operator() 是哪来的呢?我们没写,不代表编译器没写~
没错,实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义一个lambda表达式,编译器会自动生成一个类,并在类中重载operator()。
从 函数对象 和 lambda表达式 的使用来看,我不禁想起一句话 "那有什么岁月静好,只不过有人在负重前行",lambda表达式使用上确实简单方便了不少,但是实际上,该创建类还得创建类,该重载还得重载,该做的工作一样没少,只不过原来应该由我们做的事情,编译器替我们做了。使用上越来越简单。
lambda表达式类型的探究
前面说过,仿函数的缺点是一个类只能进行一个比较逻辑的判断,当比较逻辑多了,就需要我们写多个类来区分不同的仿函数对象。那lambda表达式也是按照仿函数的方式处理的,它的类型是如何区分的呢?
编译器会采用算法生成一个uuid作为lambda表达式的类型,uuid是唯一的标识符,所以,即使是书写完全相同的两个lambda表达式,类型也是不同的。需要注意的是,在语法层,lambda表达式是没有类型的。
5.lambda表达式小总结
- lambda表达式就是匿名的函数对象,要根据自定义类型的不同成员变量比较时,传仿函数 和 函数指针 比较麻烦,所以有了lambda表达式。
- lambda表达式 其实也是 新瓶装旧酒,底层是通过仿函数来实现的,如:定义了一个lambda表达式,编译器会自动生成一个类,并在类中重载operator()。
- lambda表达式的返回值只能通过 auto 来接收,这也是auto 最常用的场景之一,因为,lambda表达式的类型由编译器自动生成,在编译之前我们并不知道编译器的类型。使用auto接受,由编译器自动推导。