构造闭包: 能够捕获作用域中变量的匿名函数的对象, Lambda 表达式是纯右值表达式, 其类型是独有的无名非联合非聚合类类型, 被称为闭包类型(closure type), 所以在声明的时候必须使用 auto 来声明.
在其它语言如 lua 中, 闭包的格式相对更为简单, 可以使用 lambda 表达式作用域的所有变量, 并且返回闭包
- local function add10(arg)
- local i = 10
- local ret = function()
- i = i - 1
- return i + arg
- end
- return ret
- end
- print( add10(1)() ) -- 10
C++ 中则显得复杂些, 也提供了更多的功能来控制闭包函数的属性.
lambda 和 std::function
虽然 lambda 的使用和函数对象的调用方式有相似之处,
- std::function<int(int, int)> add2 = [&](int a, int b) -> int {
- return a + b + val + f1.value;
- };
但他们并不是同一种东西, lambda 的类型是不可知的(在编译期决定), 使用 sizeof 两者的大小也是不相同的, std::function 是函数对象, 通过消除类型再重载 operator() 达到调用的效果, 只要这个函数满足可以调用的条件, 就可以使用 std::function 保存起来, 这也是上面例子的体现.
语法 C++ 17
[ 捕获 ] ( 形参 ) 说明符(可选) 异常说明 -> ret { 函数体 }
全量声明
[ 捕获 ] ( 形参 ) -> ret { 函数体 }
const lambda 声明, 复制捕获 的对象在 lambda 体内为 const
[ 捕获 ] ( 形参 ) { 函数体 }
省略返回类型的声明, 返回的类型从函数体的返回推导
[ 捕获 ] { 函数体 }
无实参的函数
说明符 :
mutable, 允许 函数体 修改各个复制捕获的形参
constexpr C++ 17, 显式指定函数调用符为 constexpr, 当函数体满足 constexpr 函数要求时, 即使未显式指定, 也会是 constexpr
异常说明 : 提供 throw 或者 noexpect 字句
使用如下:
- struct Foo {
- int value;
- Foo() : value(1) { std::cout <<"Foo::Foo();\n"; }
- Foo(const Foo &other) {
- value = other.value;
- std::cout << "Foo::Foo(const Foo &)\n";
- }
- ~Foo() {
- value = 0;
- std::cout << "Foo::~Foo();\n";
- }
- };
- int main() {
- int val = 7;
- Foo f1;
- auto add1 = [&](int a, int b) mutable noexcept->int {
- return a + b + val + f1.value;
- };
- // 使用 std::function 包装
- std::function<int(int, int)> add2 = [&](int a, int b) -> int {
- f1.value = val; // OK, 引用捕获
- return a + b + val + f1.value;
- };
- auto add3 = [&](int a, int b) { return a + b + val + f1.value; };
- auto add4 = [=] {
- // f1.value = val; // 错误, 复制捕获 的对象在 lambda 体内为 const
- return val + f1.value;
- };
- // 全 auto 也是可以, 返回的这个 auto 不写也行
- auto add5 = [=](auto a, int b) -> auto { return a + b; };
- }
- // 输出:
- Foo::Foo();
- Foo::Foo(const Foo &)
- Foo::~Foo();
- Foo::~Foo();
Lambda 捕获
- &(以引用隐式捕获被使用的自动变量)
- =(以复制隐式捕获被使用的自动变量)
当出现任一默认捕获符时, 都能隐式捕获当前对象(this). 当它被隐式捕获时, 始终被以引用捕获, 即使默认捕获符是 = 也是如此.~~ 当默认捕获符为 = 时,(this) 的隐式捕获被弃用. (C++20 起)~~, 见 this 分析
捕获 中单独的捕获符的语法是
标识符
简单以复制捕获
标识符 ...
作为包展开的简单以复制捕获
标识符 初始化器
带初始化器的以复制捕获
& 标识符
简单以引用捕获
& 标识符 ...
作为包展开的简单引用捕获
& 标识符 初始化器
带初始化器的以引用捕获
this
当前对象的简单以引用捕获
*this
当前对象的简单以复制捕获, C++17
捕获列表可以不同的捕获方式, 当默认捕获符是 & 时, 后继的简单捕获符必须不以 & 开始, 当默认捕获符是 = 时, 后继的简单捕获符必须以 & 开始, 或者为 *this (C++17 起) 或 this (C++20 起).
在上面的示例 main 中增加, 部分代码如下, 包括了两种捕获方式, 及在函数体内修改 lambda 捕获变量的值, 及返回对象
- Foo f1;
- Foo f2;
- int val = 7;
- auto add6 = [=, &f2](int a) mutable {
- f2.value *= a;
- f1.value += f2.value + val;
- return f1;
- };
- Foo f3 = add6(3);
又到了喜闻乐见反汇编的情况了, 看看编译器是怎么实现的 lambda 表达式的.
- _ZZ4mainENUliE_clEi:
- .LFB10:
- .cfi_startproc
- pushq %rbp
- .cfi_def_cfa_offset 16
- .cfi_offset 6, -16
- movq %rsp, %rbp
- .cfi_def_cfa_register 6
- subq $32, %rsp
- movq %rdi, -8(%rbp)
- movq %rsi, -16(%rbp)
- movl %edx, -20(%rbp) // int a
- movq -16(%rbp), %rax // -16(%rbp) = & this(f2), 每次都这么赋值, 没优化的指令真的很冗余
- movq (%rax), %rax
- movl (%rax), %edx // %edx = f2.value
- movq -16(%rbp), %rax
- movq (%rax), %rax
- imull -20(%rbp), %edx // %edx = f2.value * a
- movl %edx, (%rax) // f2.value = %edx
- movq -16(%rbp), %rax
- movl 8(%rax), %edx // 在 main 函数中 -32(%rbp) + 8 = -24(%rbp) 也就是 copy 构造函数产生的 this 指针
- movq -16(%rbp), %rax // 以下的就是那些加减了,
- movq (%rax), %rax
- movl (%rax), %ecx
- movq -16(%rbp), %rax
- movl 12(%rax), %eax
- addl %ecx, %eax
- addl %eax, %edx
- movq -16(%rbp), %rax
- movl %edx, 8(%rax)
- movq -16(%rbp), %rax
- leaq 8(%rax), %rdx
- movq -8(%rbp), %rax
- movq %rdx, %rsi // 上一个 copy 构造函数内的 this 指针
- movq %rax, %rdi // copy 构造的 this 指针
- call _ZN3FooC1ERKS_ // 继续调用 copy 构造函数, 返回
- movq -8(%rbp), %rax
- leave
- .cfi_def_cfa 7, 8
- ret
- .cfi_endproc
- // lambda 的析构函数, 这个函数是隐式声明的
- _ZZ4mainENUliE_D2Ev:
- .LFB12:
- .cfi_startproc
- pushq %rbp
- .cfi_def_cfa_offset 16
- .cfi_offset 6, -16
- movq %rsp, %rbp
- .cfi_def_cfa_register 6
- subq $16, %rsp
- movq %rdi, -8(%rbp)
- movq -8(%rbp), %rax
- addq $8, %rax
- movq %rax, %rdi
- call _ZN3FooD1Ev
- nop
- leave
- .cfi_def_cfa 7, 8
- ret
- .cfi_endproc
- main:
- .LFB9:
- .cfi_startproc
- pushq %rbp
- .cfi_def_cfa_offset 16
- .cfi_offset 6, -16
- movq %rsp, %rbp
- .cfi_def_cfa_register 6
- subq $48, %rsp
- movl $7, -4(%rbp) // int val = 7;
- leaq -8(%rbp), %rax // -8(%rbp) = this(f1)
- movq %rax, %rdi
- call _ZN3FooC1Ev // Foo f1;
- leaq -12(%rbp), %rax // -12(%rbp) = this(f2)
- movq %rax, %rdi
- call _ZN3FooC1Ev // Foo f2;
- leaq -12(%rbp), %rax
- movq %rax, -32(%rbp) // -32(%rbp) = this(f2)
- leaq -8(%rbp), %rax // 取 this(f1)
- leaq -32(%rbp), %rdx
- addq $8, %rdx // copy 构造函数的 this = -24(%rbp), 记住这个 24
- movq %rax, %rsi // 第二个参数 this(f1)
- movq %rdx, %rdi // 第一个参数, 调用 copy 构造函数的 this
- call _ZN3FooC1ERKS_ // Foo(const Foo &);
- movl -4(%rbp), %eax
- movl %eax, -20(%rbp) // -20(%rbp) = 7
- leaq -36(%rbp), %rax
- leaq -32(%rbp), %rcx
- movl $3, %edx
- movq %rcx, %rsi // 第二个参数 this(f2) 的地址(两次 leaq)
- movq %rax, %rdi // 需要返回的 Foo 对象的 this 指针
- call _ZZ4mainENUliE_clEi // lambda 的匿名函数
- leaq -36(%rbp), %rax
- movq %rax, %rdi
- call _ZN3FooD1Ev
- leaq -32(%rbp), %rax
- movq %rax, %rdi
- call _ZZ4mainENUliE_D1Ev // 析构函数
- leaq -12(%rbp), %rax
- movq %rax, %rdi
- call _ZN3FooD1Ev
- leaq -8(%rbp), %rax
- movq %rax, %rdi
- call _ZN3FooD1Ev
- movl $0, %eax
- leave
- .cfi_def_cfa 7, 8
- ret
- .cfi_endproc
上面的汇编代码相对 cpp 代码还是比较多的, 由于一些隐含规则的约束下, 编译器做了很多的工作, 产生的代码的顺序就比较混乱
使用 = 值捕获时, 会先调用 copy 构造函数
使用 & 引用捕获时, 将捕获对象的引用 (地址) 作为隐式参数传给匿名函数
编译器不仅会产生匿名函数, 还会有一个析构函数产生, 这个函数负责调用在匿名函数内的析构函数
生命周期
lambda 表达式相关的对象的生命周期, 见上反汇编:
全局, 更外层作用域的生命周期不受影响
使用值捕获的情况, 先于 lambda 表达式函数体构造对象, 后于函数体执行完析构
在 lambda 表达式函数体内的对象, 在函数体执行时创建, 在闭包析构函数内析构
lambda 对象的生命周期为所在作用域结束, 析构的顺序为声明的逆序析构
this
使用 -std=c++14 生成的汇编代码在 =,&,this 捕获的情况下, 产生的汇编代码几乎一样, 都是使用的引用 (this 地址) 传参, 使用 -std=c++2a 的情况下, 编译器不推荐使用值捕获的方式(虽然还是使用的引用捕获).
TODO
补全对参数包的分析
参考
lambda 表达式 https://zh.cppreference.com/w/cpp/language/lambda ,cppreference Lambda 表达式 (C++11 起).
来源: https://www.cnblogs.com/shuqin/p/12241954.html