• C++ lambda 分析


    lambda 表达式分析

    构造闭包:能够捕获作用域中变量的匿名函数的对象,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();
    "; }
        Foo(const Foo &other) {
            value = other.value;
            std::cout << "Foo::Foo(const Foo &)
    ";
        }
        ~Foo() {
            value = 0;
            std::cout << "Foo::~Foo();
    ";
        }
    };
    
    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代码还是比较多的,由于一些隐含规则的约束下,编译器做了很多的工作,产生的代码的顺序就比较混乱

    1. 使用 = 值捕获时,会先调用copy构造函数
    2. 使用 & 引用捕获时,将捕获对象的引用(地址)作为隐式参数传给匿名函数
    3. 编译器不仅会产生匿名函数,还会有一个析构函数产生,这个函数负责调用在匿名函数内的析构函数

    生命周期

    lambda表达式相关的对象的生命周期,见上反汇编:

    1. 全局,更外层作用域的生命周期不受影响
    2. 使用值捕获的情况,先于lambda表达式函数体构造对象,后于函数体执行完析构
    3. 在lambda表达式函数体内的对象,在函数体执行时创建,在闭包析构函数内析构
    4. lambda 对象的生命周期为所在作用域结束,析构的顺序为声明的逆序析构

    this

    使用 -std=c++14 生成的汇编代码在 =&this 捕获的情况下,产生的汇编代码几乎一样,都是使用的引用(this地址)传参,使用 -std=c++2a 的情况下,编译器不推荐使用值捕获的方式(虽然还是使用的引用捕获)。

    TODO

    1. 补全对参数包的分析

    参考

    lambda 表达式,cppreference Lambda 表达式 (C++11 起)。

  • 相关阅读:
    LINUX超级用户(权限)在系统管理中的作用
    LINUX对超级用户和普通用户的理解
    LINUX设置SUID,SGID,Stick bit
    LINUX文件权限
    LINUX文件类型
    LINUX查询用户命令
    LINUX用户身份切换
    ACL权限设置
    Linux用户密码策略
    linux库列表
  • 原文地址:https://www.cnblogs.com/shuqin/p/12241954.html
Copyright © 2020-2023  润新知