Lambda表达式
需要预先了解:
1. lambda 表达式
通过使用lambda表达式可以在代码中就地定义短小的函数逻辑,同时能捕获外层作用域的变量作为内部状态,其核心语法如下:
[捕获列表](参数列表)
{
函数体
}
例如:
[]() { std::cout << "this is lambda!" << std::endl; }
1. 无参数时,括号可以省略
[] { std::cout << "this is lambda!" << std::endl; }
2. 可以在函数体后紧跟一个括号,表示直接调用该匿名函数
[] { std::cout << "this is lambda!" << std::endl; }();
2. lambda 表达式的捕获方式
2.1 值捕获
值捕获:捕获变量的副本,在lambda定义时完成拷贝。
- 显式捕获:写法为
[变量名1, 变量名2, ...],用于显式捕获外界的变量 - 隐式捕获:写法为
[=],直接捕获所有外层的可用变量
int a = 10, b = 100;
auto f1 = [a]() { return a; };
auto f2 = [=]() { return a + b; };
a = 20;
std::cout << f1(); << std::endl; // 输出10
std::cout << f2(); << std::endl; // 输出 110
2.2 引用捕获
引用捕获:捕获变量的引用
- 显式捕获:写法为
[&变量名1, &变量名2, ...],用于显式捕获外界的变量 - 隐式捕获:写法为
[&],用于隐式捕获外界所有可用变量的引用
int a = 10, b = 100;
auto f1 = [&a]() { return a; };
auto f2 = [&]() { return a + b; };
a = 20;
std::cout << f1(); << std::endl; // 输出20
std::cout << f2(); << std::endl; // 输出 120
2.3 混合捕获
混合捕获:组合 隐式全捕获 + 部分变量例外
[=, &变量1, &变量2, ...]:除变量1、变量2等单独列出的变量进行引用捕获,其余变量均进行值捕获。[&, 变量1, 变量2, ...]:除变量1、变量2等单独列出的变量进行值捕获,其余变量均进行引用捕获。- 错误写法:
[=, &],不允许同时指定两种隐式规则。
int x, y, z, w;
x = y = z = w = 10;
auto f1 = [=, &z, &w] { return x + y + z + w; };
auto f2 = [&, z, w] { return x + y + z + w; };
x += 1, y += 2, z += 3, w += 4;
std::cout << f1(); << std::endl;
std::cout << f2(); << std::endl;
3. Lambda 表达式的底层实现原理
lambda 表达式在语法上是一个匿名函数,从编译器的角度看,它会被改写成函数对象(仿函数(functor),即重载了 operator() 的类对象,允许像函数一样调用类),有以下特征:
- 函数体变成
operator()成员函数 - 捕获列表变成成员变量
- lambda 表达式本身则被替换成这个匿名类的一个对象。
3.1 编译器生成的结构
闭包类型(closure type,编译器为每个 lambda 表达式生成的匿名类)没有公开的名字,但可以手写一个等价的仿函数来对照:
// 手写的仿函数
struct Functor
{
int operator()(int x) const { return x * 2; }
};
int via_functor(int v)
{
Functor f;
return f(v);
}
// lambda 写法
int via_lambda(int v)
{
auto f = [](int x) { return x * 2; };
return f(v);
}
编译器遇到 [](int x) { return x * 2; } 时做的事情,等价于生成这样一个结构:
struct __lambda_1 // 示意名,实际没有公开的名字
{
int operator()(int x) const { return x * 2; }
};
int via_lambda(int v)
{
__lambda_1 f; // lambda 表达式被替换为匿名类的对象
return f(v); // 调用 f(v) 实际是调用 f.operator()(v)
}
用 g++ -O0 -S -masm=intel 分别编译两份代码,对比生成的汇编(为便于阅读,省略了 .seh 开头的伪指令):
; Functor::operator()(int) const
_ZNK7FunctorclEi:
push rbp
mov rbp, rsp
mov QWORD PTR 16[rbp], rcx ; this 指针
mov DWORD PTR 24[rbp], edx ; 参数 x
mov eax, DWORD PTR 24[rbp]
add eax, eax ; x * 2
pop rbp
ret
; via_lambda 内 lambda 的 operator()
_ZZ10via_lambdaiENKUliE_clEi:
push rbp
mov rbp, rsp
mov QWORD PTR 16[rbp], rcx ; this 指针
mov DWORD PTR 24[rbp], edx ; 参数 x
mov eax, DWORD PTR 24[rbp]
add eax, eax ; x * 2
pop rbp
ret
两个函数体逐条指令完全相同,区别只在符号名:lambda 的 operator() 名字里嵌入了它所在的函数名 via_lambda 和 Ul(unnamed lambda)标记,说明它是一个"寄生"在函数内部的匿名类型。调用侧的汇编也一样——f(v) 就是把对象地址作为 this 传给 operator():
mov edx, DWORD PTR 16[rbp] ; 参数 v
lea rax, -1[rbp] ; 取 lambda 对象的地址
mov rcx, rax ; 作为 this 传入
call _ZZ10via_lambdaiENKUliE_clEi
3.2 捕获列表如何变成成员变量
捕获列表中的变量会变成闭包类型的成员变量,捕获方式决定成员的内容。先看对象大小的直接验证:
#include <iostream>
int main()
{
int a = 10;
auto f0 = [](int x) { return x; }; // 无捕获
auto f1 = [a](int x) { return a + x; }; // 值捕获
auto f2 = [&a](int x) { return a + x; }; // 引用捕获
std::cout << sizeof(f0) << ' ' << sizeof(f1) << ' ' << sizeof(f2) << std::endl;
}
编译并运行代码后,看到这样的输出:
1 4 8
| 捕获方式 | 对象大小 | 成员内容 |
|---|---|---|
| 无捕获 | 1 字节 | 没有成员;空对象也占 1 字节以保证地址唯一 |
[a] 值捕获 |
4 字节 | int 成员,存 a 的副本 |
[&a] 引用捕获 |
8 字节 | 指针成员,存 a 的地址 |
再对比两种捕获方式生成的 operator()。值捕获直接从成员里读出副本:
mov rax, QWORD PTR 16[rbp] ; this
mov edx, DWORD PTR [rax] ; 读成员:a 的副本
mov eax, DWORD PTR 24[rbp] ; 参数 x
add eax, edx
引用捕获则多一次访存——先从成员里取出地址,再解引用:
mov rax, QWORD PTR 16[rbp] ; this
mov rax, QWORD PTR [rax] ; 读成员:a 的地址
mov edx, DWORD PTR [rax] ; 解引用:读 a 的值
mov eax, DWORD PTR 24[rbp] ; 参数 x
add eax, edx
成员里的地址是在 lambda 定义时写入的:
lea rax, 16[rbp] ; 取 a 的地址
mov QWORD PTR -8[rbp], rax ; 存入 lambda 对象
注:引用捕获的
operator()多一次访存,但换来的是对象里存"地址"而非"值"——这正是引用捕获能看到外部变量后续修改的原因,也是引用捕获的 lambda 不能在所引用变量失效后调用的原因。
3.3 零开销与内联
开启优化后(g++ -O2 -S -masm=intel),lambda 与手写仿函数得到完全相同的代码:
_Z10via_lambdai:
lea eax, [rcx+rcx]
ret
_Z11via_functori:
lea eax, [rcx+rcx]
ret
operator() 被内联(inline)进了调用处:没有 call 指令,也没有为 lambda 生成独立函数,整个调用只剩一条 lea。lambda 表达式不引入任何额外的运行时开销——这也正是它能直接替代手写仿函数、被 STL 算法大量使用的原因。


