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();\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 表達式,cppreference Lambda 表達式 (C++11 起)。