lambda表达式

1.C++中的可调用对象

在学习lambda表达式之前,咱们先来盘点一下C++中的那些可调用对象。

C++中的可调用对象有哪些?如下所示:

  • 函数指针 —— 类型复杂,不方便使用
  • 仿函数对象 —— 类型不同,不能复用代码
  • lambda表达式 —— 语法层没有类型,使用方便

为什么要有这么多种的可调用对象呢? 举个例子:可调用对象的发展史就好比手机的发展史;座机->按键手机->智能手机,他们都具有打电话的功能,为什么要不断地完善发展呢?说白了,就是为了方便,为了满足当今生活的需求。(博主我曾经向换回按键手机,发现根本做不到,现如今的手机和生活早已高度绑定)编程语言中特性的发展也是如此,在编程语言的不断使用和发展中,总会产生这样或那样的新需求,有了新需求,就要有新的解决措施,不然,就成历史遗留问题了。

2.函数指针

变量指针指向一个变量,数组指针指向一个数组,那,函数指针就是指向一个函数的漏喽

声明函数指针的格式:返回类型 (*指针名称)(参数类型列表); 

使用举例:使用函数指针调用add函数,完成两个整数的相加。

代码如下:

#include 
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 
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 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表达式_第1张图片可以看到 仿函数对象 和 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接受,由编译器自动推导。

你可能感兴趣的:(C++,c++)