lambda表达式

lambda

C++11支持lambda表达式。lambda表达式是C++一个非常重要的内容。

lambda表达式本质是一个匿名的函数对象 。由于lambda表达式语法上没有类型,我们就用auto或模板参数定义的对象接收lambda表达式。

与普通函数不同的是,lambda表达式可以定义在函数内部。

1、语法

cpp 复制代码
[capture-list] (parameters)-> return type {function body}

其中,

  • capture-list:捕捉列表捕捉列表不能省略。(捕捉列表具体内容下文会讲解)
  • parameters:参数列表 。lambda表达式的参数列表可以类比普通函数的参数列表。当lambda表达式设计成无参数需传入时,参数列表可以省略
  • return type:返回类型 。lambda表达式将返回类型放在函数声明的尾部,当函数的返回类型比较长的时候我们就可以这么做,使得函数名不会被遮挡。返回类型可以省略,因为编译器可以根据返回值进行推导。
  • function body:函数体 。类比普通函数的函数体。函数体不可省略

2、应用

lambda表达式出现之前,我们只能显式编写仿函数(或者使用函数指针),然后传递仿函数对象达成我们的预期:

cpp 复制代码
struct Goods
{
	string _name;
	double _price;
	int _evaluate;

	Goods(const string& name, double price, int evaluate)
		:_name(name)
		,_price(price)
		,_evaluate(evaluate)
	{}
};

struct ComparePriceLess // 价格升序
{
	bool operator() (const Goods& g1, const Goods& g2)
	{
		return g1._price < g2._price;
	}
};

struct ComparePriceGreater // 价格降序
{
	bool operator() (const Goods& g1, const Goods& g2)
	{
		return g1._price > g2._price;
	}
};

struct CompareEvaluateLess // 评价升序
{
	bool operator() (const Goods& g1, const Goods& g2)
	{
		return g1._evaluate < g2._evaluate;
	}
};

struct CompareEvaluateGreater // 评价降序
{
	bool operator() (const Goods& g1, const Goods& g2)
	{
		return g1._evaluate > g2._evaluate;
	}
};

void test2()
{
	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());    
	sort(v.begin(), v.end(), CompareEvaluateLess());    
	sort(v.begin(), v.end(), CompareEvaluateGreater()); 
}

这样一来,每次增加一个新需求就要添加一个新的仿函数,并且想搞清楚当前仿函数的实际功能就必须向上寻找仿函数,那么代码的设计就不那么简洁,代码的可读性就没有那么好。

更有甚者,如果有人对仿函数取的名字是Compare1, Compare2...那么对于后来维护代码的人来说,想要弄懂Compare1, Compare2...到底是什么意思,就必须不厌其烦地向上查找定义。

这时我们就可以利用lambda表达式:

cpp 复制代码
void test2()
{
	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());    // 价格降序
	//sort(v.begin(), v.end(), CompareEvaluateLess());    // 评价升序
	//sort(v.begin(), v.end(), CompareEvaluateGreater()); // 评价降序

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) // 价格升序
		{
			return g1._price < g2._price;
		});

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) // 价格降序
		{
			return g1._price > g2._price;
		});

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) // 评价升序
		{
			return g1._evaluate < g2._evaluate;
		});

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) // 评价降序
		{
			return g1._evaluate > g2._evaluate;
		});
}

其实上面代码对于lambda的使用,体现了模板参数定义的对象接收lambda表达式这一点。

用lambda表达式代替显式定义的仿函数,既能使代码变得更加简洁,又增加了代码的阅读性。

lambda表达式的应用很广泛,我们在接下来的学习中也会碰到。

3、捕捉列表

lambda表达式可以使用传入的参数对象,也可以使用lambda表达式所在函数域内的局部对象,这时局部对象就需要用捕捉列表进行捕捉:

捕捉列表捕捉对象的方式有值捕捉引用捕捉

  • 值捕捉的方式是直接在捕捉列表中放上对象 。值捕捉而来的对象不能直接在lambda表达式的函数体内修改
  • 引用捕捉的方式相比值捕捉添加了一个& 。引用捕捉而来的对象可以在函数体内修改,然而这种修改会在lambda函数体外造成影响。
cpp 复制代码
void test4()
{
	int i = 1;
	double d = 1.1;

	auto sum = [i, &d](int a, int b)
		{
			//++i; // 值捕捉对象,不能修改,报错
			++d;   // 引用捕捉对象,可以修改
			return i + d + a + b;
		};

	cout << sum(1, 2) << endl; // 结果是:6.1
	cout << d << endl;         // 结果是2.1
	                           // 说明lambda函数体内引用捕捉对象的修改会影响到函数体外
}

要想在lambda函数体内,修改值捕捉对象,也不是不行,只需在lambda表达式后加上关键字mutable。然而对于值捕捉对象的修改,并不会影响到lambda函数体外

cpp 复制代码
void test5()
{
	int i = 1;
	double d = 1.1;

	auto sum = [i, &d](int a, int b) mutable
		{
			++i;
			return i + d + a + b;
		};

	cout << sum(1, 2) << endl; // 结果是:6.1
	cout << i << endl;         // 结果是1
}

捕捉列表捕捉对象,还可以使用隐式捕捉的方式:

  • 捕捉列表中只给=,是对lambda外的所有对象进行值捕捉。
  • 捕捉列表中只给&,是对lambda外的所有对象进行引用捕捉。
cpp 复制代码
void test6()
{
	int i = 1;
	double d = 1.1;

	auto sum = [=]
		{
			//++i; // 值捕捉对象不能修改
			//++d; // 值捕捉对象不能修改
			return i + d;
		};

	cout << sum() << endl; // 结果是2.1
}
cpp 复制代码
void test7()
{
	int i = 1;
	double d = 1.1;

	auto sum = [&]
		{
			++i;
			++d;
			return i + d;
		};

	cout << sum() << endl; // 结果是4.1
	cout << i << endl;     // 结果是2
	cout << d << endl;     // 结果是2.1
}

实际上,隐式捕捉并不是捕捉所有对象,而是lambda使用了哪些对象,就捕捉哪些对象

捕捉列表捕捉对象的第三种方式,是值/引用捕捉与隐式捕捉的混合

  • =与&只能放在捕捉列表的前端。
  • =只能与引用捕捉组合,&只能与值捕捉组合
  • =与引用捕捉组合,引用捕捉对象之外的所有对象都是值捕捉对象;&与值捕捉组合,值捕捉对象之外的所有对象都是引用捕捉对象
cpp 复制代码
void test8()
{
	int a = 1, b = 2, c = 3, d = 4;

	auto func1 = [&, c, d]
		{
			++a, ++b;
			return a + b + c + d;
		};

	cout << func1() << endl; // 12
}
cpp 复制代码
void test9()
{
	int a = 1, b = 2, c = 3, d = 4;

	auto func1 = [=, &c, &d]
		{
			++c, d += 2;
			return a + b + c + d;
		};

	cout << func1() << endl; // 13
}

对于全局对象和静态对象,lambda捕捉列表不能捕捉它们,捕捉了也会报错;由于全局对象和静态对象在所有的作用域内都能被使用,lambda也就没有必要捕捉这些对象,因为lambda函数体内可以直接使用这些对象。

cpp 复制代码
int e = 100; // 全局对象

void test10()
{
	static int f = 1000; // 静态对象
	int a = 1, b = 2, c = 3, d = 4;

	auto func1 = [=]
		{
			return a + b + c + d + e + f;
		};

	cout << func1() << endl; // 1110
}

还有一点,lambda如果实现在全局域,捕捉列表必须为空

4、原理

开头曾提及lambda表达式本质上是一个匿名函数对象。其实,编译器在编译阶段根据lambda表达式生成了一个仿函数仿函数的名称是编译阶段确定的(比如lambda_1, lambda_2...),代码编写阶段是不知道名称的,所以我们说lambda表达式本质上是一个匿名函数对象。

lambda表达式各部分与仿函数各部分的对应关系:

部分 lambda 仿函数
函数体 lambda函数体 operator()函数体
返回类型 lambda返回类型 operator()返回类型
参数列表 lambda参数列表 operator()参数列表

捕捉列表,本质上是编译器生成的仿函数类下的成员变量。于是编译器可以利用捕捉列表里对应的对象,完成仿函数对象的初始化;再通过仿函数对象的调用、传参,完成仿函数对象的使用,即lambda的使用。

cpp 复制代码
void test11()
{
	int i = 1;
	double d = 1.1;

	auto sum = [i, d](int a, int b)
		{
			return i + d + a + b;
		};

	cout << sum(1, 2) << endl;
	cout << i << endl;         
}

struct lambda_1
{
public:
	auto operator() (int a, int b)
	{
		return i + d + a + b;
	}
private:
	int i = 1;
	double d = 1.1;
};

void test12()
{
	lambda_1 ld1;
	cout << ld1(1, 2) << endl; // sum(1, 2)的底层实现,可以认为是ld1(1, 2)
}

更进一步,我们可以认为:

  • 对于值捕捉对象,编译器生成对应仿函数类的成员变量时,赋予的是const普通类型,所以值捕捉对象在lambda函数体内不能被修改;而加上mutable修饰,可以认为是类型去掉了const,此时lambda仿函数内的值捕捉对象是外面对象的拷贝,所以lambda内对值捕捉对象的修改影响不到外面。
  • 对于引用捕捉对象,编译器赋予的是引用类型,即lambda内外的引用捕捉对象共用同一块空间,所以lambda内的修改能影响到lambda外。

lambda原理的验证,可以观察汇编代码:

相关推荐
@三十一Y1 小时前
C++:红黑树的实现
开发语言·c++
_wyt0011 小时前
拓扑排序:有向无环图的排队艺术
c++·拓扑排序·队列
皓月斯语1 小时前
B2118 验证子串
c++·题解
乐观勇敢坚强的老彭2 小时前
C++信奥:开关门、开关灯问题
开发语言·c++·算法
2301_777998343 小时前
C/C++:预处理详解
c语言·c++
t-think3 小时前
C++类和对象详解(一)
开发语言·c++
阿米亚波4 小时前
【C++ STL】std::unordered_multiset
开发语言·数据结构·c++·笔记·stl
随意起个昵称4 小时前
贪心模型-Johnson法则
c++·算法
蜡笔小马4 小时前
【保姆级教程】Windows 下 CGAL 6.2 + VS2022 全流程实战:从源码编译到项目集成(附避坑指南)
c++·cgal