需要预先了解:


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() 的类对象,允许像函数一样调用类),有以下特征:

  1. 函数体变成 operator() 成员函数
  2. 捕获列表变成成员变量
  3. 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 算法大量使用的原因。