上篇文章:C++11:新的类功能


目录

1.lambda表达式语法

2.捕捉列表

3.lambda的应用

使用仿函数举例

使用lambda表达式举例

4.lambda的原理

反汇编


1.lambda表达式语法

lambda表达式本质是一个匿名函数对象,与普通函数不同的是,它可以定义在函数内部。lambda表达式语法使用层而言,没有类型,我们一般使用auto或者模板参数定义的对象去接收lambda对象。

lambda表达式的格式:

[capture-list] (parameters)-> return type { function boby }

[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据 [ ] 来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表可以传值和传引用捕捉,捕捉列表不能为空

(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()一起省略

->return type:返回值类型,用追踪返回类型形式,声明函数的返回值类型,没有返回值时此部分可以省略一般返回值类型明确情况下,也可以省略,由编译器对返回类型进行推导

{ function boby }:函数体,函数体内的实现与普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略

int main()
{
	// 一个简单的lambda表达式
	auto add1 = [](int x, int y)->int {return x + y; };
	cout << add1(1, 2) << endl;

	// 1、捕捉为空也不能省略 
	// 2、参数为空可以省略 
	// 3、返回值可以省略,可以通过返回对象⾃动推导 
	// 4、函数体不能省略 

	auto func1 = []
		{
			cout << "111" << endl;
		};

	func1();

	int a = 0, b = 1;
	auto swap1 = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap1(a, b);
	cout << a << ":" << b << endl;

	return 0;
}

2.捕捉列表

lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域中的变量,就要进行捕捉

第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[ x, y, &z]表示x和y值捕捉,z引用捕捉。

第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表中写一个=表示隐式值捕捉,在捕捉列表写一个&表示隐式引用捕捉,这样我们lambda表达式中用了哪些变量,编译器就会自动捕捉哪些变量。

第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[ =, &x ]表示其他变量隐式值捕捉,x引用捕捉。[ &, x, y ]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉

int x = 0;
// 捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量
auto func1 = []()
	{
		x++;
	};

int main()
{
	// 只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, &b]
		{
			// 值捕捉的变量不能修改,引用捕捉的变量可以修改
			//a++;
			b++;
			int ret = a + b;
			return ret;
		};
	cout << x << endl;
	cout << func1() << endl;

	// 隐式值捕捉
	// 用了哪些变量就捕捉哪些变量
	auto func2 = [=]
		{
			x++;
			int ret = a + b + c + x;
			return ret;
		};
	cout << func2() << endl;
	
	// 隐式引用捕捉
	// 用了哪些变量就捕捉哪些变量
	auto func3 = [&]
		{
			a++;
			x++;
			d++;
		};
	func3();
	cout << a << " " << b << " " << c << " " << d  << " " << x << endl;

	// 混合捕捉1 
	auto func4 = [&, a, b]
		{
			//a++;
			//b++;
			c++;
			d++;
			return a + b + c + d;
		};
	func4();
	cout << a << " " << b << " " << c << " " << d << endl;
	
	// 混合捕捉2
	auto func5 = [=, &a, &b]
		{
			a++;
			b++;
			/*c++;
			d++;*/
			return a + b + c + d;
		};
	func5();
	cout << a << " " << b << " " << c << " " << d << endl;

	return 0;
}

运行结果:

0
2
5
1 2 2 4 2
1 2 3 5
2 3 3 5

lambda表达式如果在函数局部域中,他可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda表达式中可以直接使用。这也意味着lambda表达式如果定义在全局位置,捕捉列表必须为空

默认情况下,lambda捕捉列表是被const修饰的,也就是说传值捕捉过来的对象不能修改mutable加在参数列表的后面可以取消其常量性也就是说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改只影响形参对象,不会影响实参使用该修饰符后,参数列表不可省略(即使参数为空)。

int x = 0;
// 捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量
auto func1 = []()
	{
		x++;
	};

int main()
{
	int a = 0, b = 1, c = 2, d = 3;

	// 局部的静态和全局变量不能捕捉,也不需要捕捉 
	static int m = 0;
	func1();
	auto func6 = []
		{
			int ret = x + m;
			return ret;
		};
	cout << func6() << endl;

	// 传值捕捉本质是⼀种拷⻉,并且被const修饰了 
	// mutable相当于去掉const属性,可以修改了 
	// 但是修改了不会影响外面被捕捉的值,因为是⼀种拷⻉ 

	auto func7 = [=]()mutable
		{
			a++;
			b++;
			c++;
			d++;

			return a + b + c + d;
		};
	cout << func7() << endl;
	cout << a << " " << b << " " << c << " " << d << endl;

	return 0;
}

运行结果:

1
10
0 1 2 3

3.lambda的应用

学习lambda表达式之前,我们使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦,使用lambda去定义可调用对象,既简单又方便。

lambda在很多其他地方用起来较好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,lambda的应用还是很广泛的。

使用仿函数举例

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;
    }
};

struct CompareEvaluateLess
{
    bool operator()(const Goods& gl, const Goods& gr)
    {
        return gl._evaluate < gr._evaluate;
    }
};

struct CompareEvaluateGreater
{
    bool operator()(const Goods& gl, const Goods& gr)
    {
        return gl._evaluate > gr._evaluate;
    }
};

void PrintGoods(const vector<Goods>& v) {
	for (const auto& good : v) {
		cout << "[" << good._name << "] 价格: " << good._price << ", 评价: " << good._evaluate << endl;
	}
	cout << "-------------------------" << endl;
}

int main()
{
    vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } };

	cout << "初始数据:" << endl;
	PrintGoods(v);

	// 1. 按价格升序
	sort(v.begin(), v.end(), ComparePriceLess());
	cout << "按价格 升序 排序后:" << endl;
	PrintGoods(v);

	// 2. 按价格降序
	sort(v.begin(), v.end(), ComparePriceGreater());
	cout << "按价格 降序 排序后:" << endl;
	PrintGoods(v);

	// 3. 按评价升序
	sort(v.begin(), v.end(), CompareEvaluateLess());
	cout << "按评价 升序 排序后:" << endl;
	PrintGoods(v);

	// 3. 按评价降序
	sort(v.begin(), v.end(), CompareEvaluateGreater());
	cout << "按评价 降序 排序后:" << endl;
	PrintGoods(v);
	
	return 0;
}

运行结果:

初始数据:
[苹果] 价格: 2.1, 评价: 5
[香蕉] 价格: 3, 评价: 4
[橙子] 价格: 2.2, 评价: 3
[菠萝] 价格: 1.5, 评价: 4
-------------------------
按价格 升序 排序后:
[菠萝] 价格: 1.5, 评价: 4
[苹果] 价格: 2.1, 评价: 5
[橙子] 价格: 2.2, 评价: 3
[香蕉] 价格: 3, 评价: 4
-------------------------
按价格 降序 排序后:
[香蕉] 价格: 3, 评价: 4
[橙子] 价格: 2.2, 评价: 3
[苹果] 价格: 2.1, 评价: 5
[菠萝] 价格: 1.5, 评价: 4
-------------------------
按评价 升序 排序后:
[橙子] 价格: 2.2, 评价: 3
[香蕉] 价格: 3, 评价: 4
[菠萝] 价格: 1.5, 评价: 4
[苹果] 价格: 2.1, 评价: 5
-------------------------
按评价 降序 排序后:
[苹果] 价格: 2.1, 评价: 5
[香蕉] 价格: 3, 评价: 4
[菠萝] 价格: 1.5, 评价: 4
[橙子] 价格: 2.2, 评价: 3
-------------------------

使用lambda表达式举例

// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中
// 不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了

struct Goods
{
	string _name; // 名字 
	double _price; // 价格 
	int _evaluate; // 评价 
	// ...
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{
	}
};

void PrintGoods(const vector<Goods>& v) {
	for (const auto& good : v) {
		cout << "[" << good._name << "] 价格: " << good._price << ", 评价: " << good._evaluate << endl;
	}
	cout << "-------------------------" << endl;
}

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3
}, { "菠萝", 1.5, 4 } };

	// 1. 按价格升序
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price < g2._price;
		});
	cout << "按价格 升序 排序后:" << endl;
	PrintGoods(v);

	// 2. 按价格降序
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price > g2._price;
		});
	cout << "按价格 降序 排序后:" << endl;
	PrintGoods(v);

	// 3. 按评价升序
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate < g2._evaluate;
		});
	cout << "按评价 升序 排序后:" << endl;
	PrintGoods(v);

	// 4. 按评价降序
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate > g2._evaluate;
		});
	cout << "按评价 降序 排序后:" << endl;
	PrintGoods(v);

	return 0;
}

4.lambda的原理

lambda 的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个Lambda以后,编译器会生成一个对应的仿函数的类。

仿函数的类名是编译按一定规则生成的,保证不同的lambda生成的类名不同lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。

上面的原理,我们可以透过汇编层了解一下,下面第二段汇编层代码印证了上面的原理。

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.49;

	// lambda
	auto r2 = [rate](double money, int year) {
		return money * rate * year;
		};

	// 函数对象 
	Rate r1(rate);
	cout << r1(10000,2) << endl;

	cout << r2(10000, 2) << endl;

	auto func1 = [] {
		cout << "hello world" << endl;
		};
	func1();

	return 0;
}

反汇编

	// lambda
	auto r2 = [rate](double money, int year) {
00B724C0  push        ebp  
00B724C1  mov         ebp,esp  
00B724C3  sub         esp,0D4h  
00B724C9  push        ebx  
00B724CA  push        esi  
00B724CB  push        edi  
00B724CC  push        ecx  
00B724CD  lea         edi,[ebp-14h]  
00B724D0  mov         ecx,5  
00B724D5  mov         eax,0CCCCCCCCh  
00B724DA  rep stos    dword ptr es:[edi]  
00B724DC  pop         ecx  
00B724DD  mov         dword ptr [this],ecx  
00B724E0  mov         ecx,offset _D4C5C408_Test@cpp (0B7F084h)  
00B724E5  call        @__CheckForDebuggerJustMyCode@4 (0B71398h)  
00B724EA  nop  
		return money * rate * year;
00B724EB  mov         eax,dword ptr [this]  
00B724EE  movsd       xmm0,mmword ptr [money]  
00B724F3  mulsd       xmm0,mmword ptr [eax]  
00B724F7  cvtsi2sd    xmm1,dword ptr [year]  
00B724FC  mulsd       xmm0,xmm1  
00B72500  movsd       mmword ptr [ebp-0D4h],xmm0  
00B72508  fld         qword ptr [ebp-0D4h]  
		};
00B7250E  pop         edi  
00B7250F  pop         esi  
00B72510  pop         ebx  
00B72511  add         esp,0D4h  
00B72517  cmp         ebp,esp  
00B72519  call        __RTC_CheckEsp (0B713CFh)  
00B7251E  mov         esp,ebp  
00B72520  pop         ebp  
00B72521  ret         0Ch  

讲解反汇编代码:

00B724C0  push        ebp              ; 1. 保存旧的栈底指针
00B724C1  mov         ebp,esp          ; 2. 设置新的栈底(栈帧建立)
00B724C3  sub         esp,0D4h         ; 3. 分配局部变量空间(包含调试用的缓冲区)
00B724C9  push        ebx              ; 4. 保存非易失性寄存器(惯例)
00B724CA  push        esi  
00B724CB  push        edi  
00B724CC  push        ecx              ; 5. 【关键】压入 ecx。还记得吗?ecx 存的是 this 指针!
00B724CD  lea         edi,[ebp-14h]    ; \
00B724D0  mov         ecx,5             ;  | (这部分是 VS 的 RTC 检查,用 0xCC 填充内存,
00B724D5  mov         eax,0CCCCCCCCh    ;  |  用于检测越界,不用深究)
00B724DA  rep stos    dword ptr es:[edi]; /
00B724DC  pop         ecx              ; 6. 恢复 ecx(重新拿到 this 指针)
00B724DD  mov         dword ptr [this],ecx ; 7. 把 this 指针存到一个名为 [this] 的临时变量里


00B724EA  nop                       ; 调试占位符

; 步骤 1:取出 this 指针
00B724EB  mov         eax,dword ptr [this]  ; eax = this 指针(指向 r2 对象的首地址)

; 步骤 2:加载第一个参数 money
00B724EE  movsd       xmm0,mmword ptr [money] ; xmm0 = money (10000.0)
                                                ; movsd 是移动双精度浮点数 (64bit)

; 步骤 3:【核心印证】访问成员变量 rate
00B724F3  mulsd       xmm0,mmword ptr [eax]   ; xmm0 = xmm0 * [eax]
                                                ; 【解释】
                                                ; eax 存的是 this 指针。
                                                ; [eax] 就是取 this 指向内存的前 8 个字节。
                                                ; 这正是我们按值捕捉的成员变量 _rate (0.49)!
                                                ; 此时 xmm0 = 10000.0 * 0.49

; 步骤 4:加载第二个参数 year (int -> double)
00B724F7  cvtsi2sd    xmm1,dword ptr [year]    ; cvtsi2sd = Convert Integer to Scalar Double
                                                ; xmm1 = (double)year (即 2.0)

; 步骤 5:最后一步乘法
00B724FC  mulsd       xmm0,xmm1                 ; xmm0 = (10000*0.49) * 2

; 步骤 6:准备返回值
00B72500  movsd       mmword ptr [ebp-0D4h],xmm0 ; 先把结果存回临时内存
00B72508  fld         qword ptr [ebp-0D4h]       ; 再把结果压入浮点栈 st(0)
                                                    ; (这是 x86 下 double 返回值的标准传递方式)


00B7250E  pop         edi              ; 恢复寄存器
00B7250F  pop         esi  
00B72510  pop         ebx  
00B72511  add         esp,0D4h         ; 回收局部变量空间
00B72517  cmp         ebp,esp          ; 检查栈是否平衡(RTC 调试)
00B72519  call        __RTC_CheckEsp (0B713CFh)
00B7251E  mov         esp,ebp          ; 销毁栈帧
00B72520  pop         ebp  
00B72521  ret         0Ch              ; 【关键】返回并清栈
                                        ; ret 0Ch = ret + add esp, 12
                                        ; 为什么是 12 字节?
                                        ; 参数 money (double, 8字节) + year (int, 4字节) = 12字节。
                                        ; 这完美符合函数签名 `double lambda(double, int)`。

本章完。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐