C++ lambda 分析

lambda 表達式分析

構造閉包:可以捕獲做用域中變量的匿名函數的對象,Lambda 表達式是純右值表達式,其類型是獨有的無名非聯合非聚合類類型,被稱爲閉包類型(closure type),因此在聲明的時候必須使用 auto 來聲明。c++

在其它語言如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保存起來,這也是上面例子的體現。this

語法 C++ 17

  1. [ 捕獲 ] ( 形參 ) 說明符(可選) 異常說明 -> ret { 函數體 }
    • 全量聲明
  2. [ 捕獲 ] ( 形參 ) -> ret { 函數體 }
    • const lambda 聲明,複製捕獲 的對象在 lambda 體內爲 const
  3. [ 捕獲 ] ( 形參 ) { 函數體 }
    • 省略返回類型的聲明,返回的類型從函數體的返回推導
  4. [ 捕獲 ] { 函數體 }
    • 無實參的函數

說明符lua

  • mutable, 容許 函數體 修改各個複製捕獲的形參
  • constexpr C++ 17, 顯式指定函數調用符爲 constexpr,當函數體知足 constexpr函數要求時,即便未顯式指定,也會是 constexpr

異常說明 :提供 throw 或者 noexpect 字句spa

使用以下:指針

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分析
捕獲 中單獨的捕獲符的語法是code

  1. 標識符
    • 簡單以複製捕獲
  2. 標識符 ...
    • 做爲包展開的簡單以複製捕獲
  3. 標識符 初始化器
    • 帶初始化器的以複製捕獲
  4. & 標識符
    • 簡單以引用捕獲
  5. & 標識符 ...
    • 做爲包展開的簡單引用捕獲
  6. & 標識符 初始化器
    • 帶初始化器的以引用捕獲
  7. this
    • 當前對象的簡單以引用捕獲
  8. *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 起)。

相關文章
相關標籤/搜索