C++11新特性(3):lambda不是玄学:从编译器生成的仿函数类彻底搞懂 C++ 匿名函数

C++11新特性(3):lambda不是玄学:从编译器生成的仿函数类彻底搞懂 C++ 匿名函数

  • 一、lambda表达式语法
    • [1.1 lambda表达式的类型和本质](#1.1 lambda表达式的类型和本质)
    • [1.2 lambda表达式的写法和含义](#1.2 lambda表达式的写法和含义)
  • 二、捕捉列表
    • [2.1 捕捉方式](#2.1 捕捉方式)
      • [2.2.1 显式捕捉](#2.2.1 显式捕捉)
      • [2.2.2 隐式捕捉](#2.2.2 隐式捕捉)
      • [2.2.3 混合捕捉](#2.2.3 混合捕捉)
    • [2.2 mutable关键字改变常性](#2.2 mutable关键字改变常性)
  • 三、lambda的原理
    • [3.1 lambda表达式其实就是让编译器自己生成一个仿函数](#3.1 lambda表达式其实就是让编译器自己生成一个仿函数)
    • [3.2 从汇编层来看lambda的原理](#3.2 从汇编层来看lambda的原理)
    • [3.2.1 补充知识:UUID---唯一识别码](#3.2.1 补充知识:UUID---唯一识别码)
    • [3.2.2 lambda的调用过程](#3.2.2 lambda的调用过程)
      • [3.2.1 编译时:捕获、生成匿名类、赋值给auto对象](#3.2.1 编译时:捕获、生成匿名类、赋值给auto对象)
      • [3.2.2 运行时:调用匿名类的operator()函数](#3.2.2 运行时:调用匿名类的operator()函数)
  • 四、下节预告


递归何不归:个人主页
个人专栏 : 《C++庖丁解牛》《数据结构详解》

在广袤的空间和无限的时间中,能与你共享同一颗行星和同一段时光,是我莫大的荣幸

一、lambda表达式语法

1.1 lambda表达式的类型和本质

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

提前说一嘴 : 在底层角度来看,每一个lambda表达式都是一个独特的类型

1.2 lambda表达式的写法和含义

lambda表达式的格式:

cpp 复制代码
[capture-list] (parameters)-> return type { function boby }
组成部分 格式/示例 是否可省略 说明
捕捉列表 [ ][=][&][a, &b] 不能省略 必须出现在 lambda 开始位置,编译器根据 [] 判断 lambda 表达式。用于捕获上下文中的变量,支持传值或传引用捕捉。即使为空 [] 也不能省略。
参数列表 (int x, int y) 可以省略 与普通函数参数列表功能相同。如果不需要参数,可以连同 () 一起省略。
返回值类型 -> int-> double 通常可省略 使用追踪返回类型形式声明。无返回值时可省略;返回值类型明确时,编译器可自动推导,也可省略。
函数体 { return x + y; } 不能省略 实现具体功能。可以使用参数和捕获到的变量。即使函数体为空 {} 也不能省略。

使用示例:

cpp 复制代码
```cpp
// 完整形式(什么都不省略)
auto add1 = [](int x, int y) -> int { return x + y; };

// 省略参数列表(参数为空)
auto func1 = [] { cout << "hello" << endl; };

// 省略返回值类型(编译器推导)
auto add2 = [](int x, int y) { return x + y; };  // 推导为 int

二、捕捉列表

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

对于全局变量和局部静态变量,不能也无需捕捉 ,在lambda表达式中可以直接使用

2.1 捕捉方式

捕捉方式主要有两种,一种是传值捕捉,一种是引用捕捉,传值捕捉就是创建一个捕捉对象的拷贝 (不会影响被捕捉对象),引用捕捉就是直接应用捕捉对象 (会影响被捕捉对象)

2.2.1 显式捕捉

顾名思义,显式捕捉就是显式写出捕捉的对象和方式,是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x, y,&z]表示x和y值捕捉,z 引用捕捉。

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

2.2.2 隐式捕捉

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

需要注意的是,此处的隐式捕捉中,编译器是按需捕捉的,只会捕捉在编译器中用到的变量

cpp 复制代码
// 隐式值捕捉
// 用了哪些变量就捕捉哪些变量
auto func2 = [=]
{
	int ret = a + b + c;
	return ret;
};
cout << func2() << endl;

// 隐式引用捕捉
// 用了哪些变量就捕捉哪些变量
auto func3 = [&]
{
	a++;
	c++;
	//d++;
};

2.2.3 混合捕捉

混合捕捉是在捕捉列表中混合使用隐式捕捉和显示捕捉 。[=,&x]表示其他变量隐式值捕捉, x 引用捕捉 ;[&,x,y]表示其他变量引用捕捉,x和y值捕捉 。当使用混合捕捉时,第一个元素必须是&或=,并且**&混合捕捉时**,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉==(也就是前后类型必须是不一样的,否则混合捕捉就失去了意义 ==。

cpp 复制代码
// 混合捕捉1
auto func4 = [&, a, b]
{
	//a++;
	//b++;
	c++;
	d++;
	return a + b + c + d;
};

2.2 mutable关键字改变常性

默认情况下,lambda 捕捉列表是被const修饰的 ,也就是说传值捕捉的过来的对象不能修改,

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

cpp 复制代码
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b](int x)mutable
{
	// 值捕捉的变量不能修改,引用捕捉的变量可以修改
	a++;
	b++;
	int ret = a + b + x + y;
	return ret;
};

三、lambda的原理

3.1 lambda表达式其实就是让编译器自己生成一个仿函数

cpp 复制代码
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);
	r1(10000, 2);
	r2(10000, 2);

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

	func1();

	return 0;
}

我们在r1和r2调用处打上断点

转到反汇编可以看到:

我们可以看到:r1仿函数调用的是他的operator()函数 ,但是r2 lamda表达式调用的也是operator()函数,这说明lambda表达式 底层其实就是转换成仿函数调用

3.2 从汇编层来看lambda的原理

3.2.1 补充知识:UUID---唯一识别码

在lambda表达式生成过程中,编译器会生成一个这个lambda表达式的唯一码(uuid)来标记它的类型 ,这也是为什么lambda表达式在使用层没有类型

3.2.2 lambda的调用过程

这一段写不动了,是ai写的,直接cv了

3.2.1 编译时:捕获、生成匿名类、赋值给auto对象

编译过程:

  1. 生成唯一类型

    编译器为每个 lambda 生成一个独一无二的匿名类 (类名通常包含 UUID,如 __lambda_xxxx)。这也是为什么 lambda "语法上没类型"------类型有,但名字你写不出来。

  2. 处理捕捉列表

    捕捉列表中的变量变成匿名类的成员变量 ,在构造时初始化

    • [x] → 成员变量 _x,拷贝构造
    • [&x] → 成员变量 &_x,引用绑定

    这个过程发生在lambda 定义时(编译期确定布局,运行时初始化)。

  3. 生成 operator()

    函数体被放入 operator() 中。这个函数不是虚函数,地址在编译时完全确定。

3.2.2 运行时:调用匿名类的operator()函数

  • 调用时(如 r2(10000, 2)),编译器已经知道要调用 __lambda_xxx::operator()
  • CPU 直接跳转到那个固定地址,把参数压栈,执行函数体
  • 没有查表、没有间接调用、没有运行时类型判断

四、下节预告

我们今天了解了一下lambda表达式的,但是lambda表达式还是存在一些问题:

lambda表达式在语法上是没有类型的 ,这也意味着它很难装进容器中(毕竟类型不确定),我们可不可以在lambda外面封装一层,使lambda拥有显式的类型呢?

预知后事如何,请听下回分解

相关推荐
SilentSamsara2 小时前
综合实战:用 Python 做一个待办事项管理器(CLI 版)
开发语言·python·青少年编程·pycharm
HAPPY酷2 小时前
UE5 C++ 避坑指南:暴力移除 Electronic Nodes 插件,回归纯净开发
开发语言·c++·ue5
huipeng9262 小时前
分布式服务部署详解
java·开发语言·spring cloud·微服务
eqwaak02 小时前
4 月技术快讯|Rust 1.90 正式发布,系统级开发再进化
开发语言·后端·rust
小此方2 小时前
Re:思考·重建·记录 现代C++ C++11篇 (四)C++ Lambda 全解析:编译器是如何为你生成仿函数的?
开发语言·c++·c++11·现代c++
Brilliantwxx2 小时前
【C++】初认识模版
开发语言·c++
c++之路2 小时前
C++ 命名空间(Namespace)
开发语言·c++·算法
艾莉丝努力练剑3 小时前
【Linux网络】计算机网络入门:Socket编程预备,从字节序共识到 Socket 地址结构的“伪多态”设计
linux·服务器·网络·c++·学习·计算机网络
2501_933329559 小时前
媒介宣发技术实践:Infoseek舆情系统的AI中台架构与应用解析
开发语言·人工智能·架构·数据库开发