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对象
编译过程:
-
生成唯一类型
编译器为每个 lambda 生成一个独一无二的匿名类 (类名通常包含 UUID,如
__lambda_xxxx)。这也是为什么 lambda "语法上没类型"------类型有,但名字你写不出来。 -
处理捕捉列表
捕捉列表中的变量变成匿名类的成员变量 ,在构造时初始化。
[x]→ 成员变量_x,拷贝构造[&x]→ 成员变量&_x,引用绑定
这个过程发生在lambda 定义时(编译期确定布局,运行时初始化)。
-
生成
operator()函数体被放入
operator()中。这个函数不是虚函数,地址在编译时完全确定。
3.2.2 运行时:调用匿名类的operator()函数
- 调用时(如
r2(10000, 2)),编译器已经知道要调用__lambda_xxx::operator() - CPU 直接跳转到那个固定地址,把参数压栈,执行函数体
- 没有查表、没有间接调用、没有运行时类型判断
四、下节预告
我们今天了解了一下lambda表达式的,但是lambda表达式还是存在一些问题:
lambda表达式在语法上是没有类型的 ,这也意味着它很难装进容器中(毕竟类型不确定),我们可不可以在lambda外面封装一层,使lambda拥有显式的类型呢?
预知后事如何,请听下回分解