Constructs a closure: an unnamed function object capable of capturing variables in scope.html
—— Lambda functions (since C++11) [cppreference.com]express
按照C++11標準的說法,lambda表達式的標準格式以下:數組
[ capture ] ( params ) mutable exception attribute -> ret { body } // (1) 完整的聲明 [ capture ] ( params ) -> ret { body } //(2) 一個常lambda的聲明:按副本捕獲的對象不能被修改。 [ capture ] ( params ) { body } // (3) 省略後綴返回值類型:閉包的operator()的返回值類型是根據如下規則推導出的:若是body僅包含單一的return語句,那麼返回值類型是返回表達式的類型(在此隱式轉換以後的類型:右值到左值、數組與指針、函數到指針)不然,返回類型是void [ capture ] { body } //(4) 省略參數列表:函數沒有參數,即參數列表是()
capture - 指定哪些在函數聲明處的做用域中可見的符號將在函數體內可見。閉包
符號表可按以下規則傳入:函數
[a,&b],按值捕獲a,並按引用捕獲bthis
[this],按值捕獲了this指針spa
[&] 按引用捕獲在lambda表達式所在函數的函數體中說起的所有自動儲存持續性變量.net
[=] 按值捕獲在lambda表達式所在函數的函數體中說起的所有自動儲存持續性變量翻譯
[] 什麼也沒有捕獲debug
params - 參數列表,與命名函數同樣
ret - 返回值類型。若是不存在,它由該函數的return語句來隱式決定(或者是void,例如當它不返回任何值的時候)
body - 函數體
下面,我將從最簡單的形式開始逐步對各類形式的lambda表達式進行彙編分析。
首先是最簡單的類型(4):
和普通表達式同樣,若單純的一個表達式將被編譯器忽略,這裏將lambda表達式賦值給一個棧變量進行分析。
int main() { auto lambda = []{ }; return 0; }
IntelliSense顯示這裏的lambda變量實際上是一個 void lambda(),編譯後被解析是main::__l3::void<lambda>(void)類型,debug查看彙編代碼,發現本句並無在main函數裏產生任何彙編代碼,但並不表明這個表達式沒有意義,
...省略... auto lambda = []{ }; return 0; xor eax,eax } ...省略...
若使用sizeof(lambda)計算其所佔字節數將獲得1,稍微在main代碼上面一點,能夠發現[]{}是做爲一個函數被編譯:
push ebp mov ebp,esp sub esp,0CCh push ebx push esi push edi push ecx lea edi,[ebp-0CCh] mov ecx,33h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] pop ecx mov dword ptr [this],ecx pop edi pop esi pop ebx mov esp,ebp pop ebp ret int 3 int 3
可見,就像普通函數同樣,[]{}表達式內部被編譯爲一個函數,該函數內有一個this指針做爲棧變量,它指向調用函數時的寄存器ecx。
下面咱們執行這個lambda表達式,進入閉包內部分析,同時,爲了好說明,在函數內增長一條賦值語句。
int main() { auto lambda = []{ int s = 0xA; }; lambda(); return 0; }
對應有彙編代碼:
auto lambda = []{ int s = 0xA; }; lambda(); lea ecx,[ebp-5] call 001E1570 return 0;
能夠看到,有一個地址傳送,[ebp-5]的地址送給ecx,而後直接調用閉包函數。
[ebp-5]是main的一個棧變量,佔用4字節,他的值沒有被初始化,debug版本默認是(0xcccccccc)。
將其地址&[ebp-5]送入ecx究竟有什麼含義,不妨先進入閉包函數內部看看:
push ebp mov ebp,esp sub esp,0D8h push ebx push esi push edi push ecx lea edi,[ebp+FFFFFF28h] mov ecx,36h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] pop ecx mov dword ptr [ebp-8],ecx int s = 0xA; mov dword ptr [ebp-14h],0Ah }; pop edi pop esi pop ebx mov esp,ebp pop ebp ret
可見,剛纔的ecx被push保存,而後又在函數初始化棧完成後(rep stos後),被彈出並寫入局部變量[ebp-8]中,而這個[ebp-8]其實就是上面說到的this指針。也就是說,這個this指針指向main中的一個局部變量。
那麼,爲了進一步研究這個機制,咱們設法讓這個閉包使用this。不妨猜測一下,this既然是指向main裏面的變量,那麼他多是一個base address用來「捕獲」(lambda中的概念)閉包外層做用域內的某些變量。「捕獲」方式在上面有說到,若將上面的[]改成[=],讓lambda按值捕獲main中的int變量s,再看看有什麼變化:
int main() { int a = 0xB; auto lambda = [=]{ int s = a; }; lambda(); return 0; }
閉包內對應彙編代碼:
pop ecx mov dword ptr [ebp-8],ecx int s = a; mov eax,dword ptr [ebp-8] mov ecx,dword ptr [eax] mov dword ptr [ebp-14h],ecx };
一樣的,先放置this指針,而後下面比較關鍵:
把this臨時放到eax
而後再取eax地址對應的值放到臨時ecx寄存器中,這裏就是a
而後賦值給[ebp-14h]就是s
那麼繞了半天作了什麼事,其實就是至關於下面的代碼:
s = *this;
那麼這個this確實是指向了main裏面的a,如何辦到的?
查看main棧內存發現,傳給閉包的this是指向下圖中選中部分,而紅框中是變量a:
可見,a在main的棧空間被複制了一次,而不是閉包的棧空間,那麼複製發生在哪一個時候,爲何this剛好就指向了a的副本?
再調用閉包函數以前,還作了一些事情:
int a = 0xB; mov dword ptr [ebp-8],0Bh auto lambda = [=]{ int s = a; }; lea eax,[ebp-8] push eax lea ecx,[ebp-14h] call 010E1BE0 lambda(); lea ecx,[ebp-14h] call 010E1C20 return 0;
發現還call了一個帶參函數:
將a的地址送入eax並壓棧,至關於給下面的函數傳參&a
將給後面閉包用的this保存在ecx中,可能會給下面的一個call使用
上面的操做至關於下面的僞代碼:
call 010E1BE0( &a , this); //固然,this並非做爲參數傳入的,這裏只是方便理解
能夠預見,010E1BE0函數的做用應該是拷貝a,並讓this指向a,空口無憑,進去看看:
push ebp mov ebp,esp sub esp,0CCh push ebx push esi push edi push ecx lea edi,[ebp+FFFFFF34h] mov ecx,33h mov eax,0CCCCCCCCh rep stos dword ptr es:[edi] pop ecx mov dword ptr [ebp-8],ecx mov eax,dword ptr [ebp-8] mov ecx,dword ptr [ebp+8] mov edx,dword ptr [ecx] mov dword ptr [eax],edx mov eax,dword ptr [ebp-8] pop edi pop esi pop ebx mov esp,ebp pop ebp ret 4
先後的代碼循序漸進,主要是中間:
ecx是this不用說了。
先把this保存到該函數的棧空間再說
this放進eax,預見下面的[eax]就是*this,和上面說到的同樣
而後是[ebp+8]這塊,送給ecx臨時保存,而後取值,送入edx臨時保存,可見[ebp+8]裏面應該是一個地址
edx送給*this
最後那個mov eax,[ebp-8] ,又把this做爲返回值
關於[ebp+8]:還記得傳入該函數的參數&a嗎?沒錯,[ebp+8]保存的是就是&a。
簡單翻譯一下這個函數的意思:
fun(&a,this);
int* fun(int* in,int* this)
{
*this = *in;
return this;
}
注意這裏的this傳遞實際上是經過寄存器的方式。
好了,說了半天,剛纔那個問題,差很少也知道答案了。
調用閉包函數前,「捕獲者」this指針被放在main中,並對其指向的內存塊拷貝閉包中要用到的變量值,調用時,this經過寄存器送入閉包中,閉包經過this訪問外層做用域(這裏是main)的已捕獲對象(這裏是a)。
可見,若是閉包要按值捕獲main中多個變量,那麼事先要調用一個複製函數,依次複製全部要用的變量,而後經過this尋址訪問main中變量的副本,而不是把全部變量拷貝到閉包的棧空間內。
上面說的都是最簡單的形式,也即:[=]{ },以後的文章將分析更復雜的lambda表達式。今天先說到這。
參考資料: