1. 程式人生 > >C++中lambda表示式詳解與原理分析

C++中lambda表示式詳解與原理分析

lambda表示式的本質就是過載了()運算子的類,這種類通常被稱為functor,即行為像函式的類。因此lambda表示式物件其實就是一個匿名的functor。

C++中lambda表示式的構成

一個標準的lambda表示式包括:捕獲列表、引數列表、mutable指示符、尾置返回型別(->返回型別)和函式體:

[capture list] (params list) mutable exception-> return type { function body }

各項具體含義如下

  1. capture list:捕獲外部變數列表
  2. params list:形參列表
  3. mutable指示符:用來說用是否可以修改捕獲的變數
  4. exception:異常設定
  5. return type:返回型別
  6. function body:函式體

此外,我們還可以省略其中的某些成分來宣告“不完整”的lambda表示式,常見的有以下幾種:

序號 格式
1 [capture list] (params list) -> return type {function body}
2 [capture list] (params list) {function body}
3 [capture list] {function body}

其中:

  • 格式1聲明瞭const型別的表示式,這種型別的表示式不能修改捕獲列表中的值。
  • 格式2省略了返回值型別,但編譯器可以根據以下規則推斷出Lambda表示式的返回型別:
    1. 如果function body中存在return語句,則該Lambda表示式的返回型別由return語句的返回型別確定
    2. 如果function body中沒有return語句,則返回值為void型別。
  • 格式3中省略了引數列表,類似普通函式中的無參函式。

簡單lambda表示式及其原理分析

在VS2017中構造一個簡單的lambda表示式如下:

auto f = [] (int a, int b) -> int
{
        return a + b + 42;
};
003A3BC0 6
A 03 push 3 003A3BC2 6A 04 push 4 003A3BC4 8D 4D EB lea ecx,[f] 003A3BC7 E8 04 E4 FF FF call <lambda_f2fe7ac06244f603e089b2eaef4ffd5c>::operator() (03A1FD0h) cout << f(4, 3) << endl;

可以看到,呼叫該functor時,call到的是一個lambda物件的operator()位置,該位置反彙編程式碼如下:

00BA1FD0 55                   push        ebp  
00BA1FD1 8B EC                mov         ebp,esp  
00BA1FD3 81 EC CC 00 00 00    sub         esp,0CCh  
; 省略一堆東西
00BA1FF0 89 4D F8             mov         dword ptr [this],ecx  
    49:         return a + b + 42;
00BA1FF3 8B 45 0C             mov         eax,dword ptr [b]  
00BA1FF6 8B 4D 08             mov         ecx,dword ptr [a]  
00BA1FF9 8D 44 01 2A          lea         eax,[ecx+eax+2Ah]  
    50:     };
; 省略一堆東西
00BA2000 8B E5                mov         esp,ebp  
00BA2002 5D                   pop         ebp  
00BA2003 C2 08 00             ret         8  

注意:

  1. 如果在lambda表示式中忽略括號和形參列表,則相當於指定的函式沒有入參。
  2. lambda表示式中不能指定引數的預設值。
  3. 如果忽略返回值型別,則由編譯器做自動型別推斷,詳細規則如第一部分所示。

捕獲列表

由於lambda表示式是在某函式內定義的,因此我們可能希望其能使用函式內的區域性變數,這時則可以使用所謂捕獲列表。總體說來,一共有三種捕獲方式:值捕獲、引用捕獲和外部捕獲。

值捕獲

值捕獲和引數傳遞中的值傳遞類似,被捕獲的變數的值在Lambda表示式建立時通過值拷貝的方式傳入,因此隨後對該變數的修改不會影響影響Lambda表示式中的值。在VS2017中建立如下程式碼並加以分析:

int testFunc1()
{
    int nTest1 = 23;

    auto f = [nTest1] (int a, int b) -> int
    {
        return a + b + 42 + nTest1;
        //nTest1 = 333;              不能在lambda表示式中修改捕獲變數的值
    };

    cout << f(4, 3) << "&nTest1=" << nTest1 << endl;
}

需要注意的是,不能在lambda表示式中修改捕獲變數的值。其實根據上面的反彙編分析,我們已經知道,lambda表示式中的程式碼是在一個單獨的函式中執行的,這個函式在呼叫時建立了自己的棧幀,而其使用的nTest1區域性變數在testFunc1的棧幀中,雖然通過ebp可以進行棧幀回溯,但顯然這是一種不合情理的做法。因此可以斷定,捕獲列表中出現的區域性變數一定會通過某種方式傳遞給lambda匿名類。到底採用的是哪種方式呢?我們來揭曉答案:

    49:     auto f = [nTest1] (int a, int b) -> int
    50:     {
    51:         return a + b + 42 + nTest1;
    52:     };
01183C08 8D 45 E8             lea         eax,[nTest1]  
01183C0B 50                   push        eax  
01183C0C 8D 4D DC             lea         ecx,[f]  
01183C0F E8 0C E3 FF FF       call        <lambda_ed51e51ff76776313a28b716c94bbc2d>::<lambda_ed51e51ff76776313a28b716c94bbc2d> (01181F20h)  
    ; 省略無關程式碼
    54:     cout << f(4, 3) << "&nTest1=" << nTest1 << endl;
01183C1D 8B 45 E8             mov         eax,dword ptr [nTest1]  
01183C20 50                   push        eax  
01183C21 68 40 BE 18 01       push        offset string "&nTest1=" (0118BE40h)  
01183C26 6A 03                push        3  
01183C28 6A 04                push        4  
01183C2A 8D 4D DC             lea         ecx,[f]  
01183C2D E8 4E F7 FF FF       call        <lambda_ed51e51ff76776313a28b716c94bbc2d>::operator() (01183380h)  

基本可以斷定,nTest是在lambda匿名類構造時傳入的。並且傳入的是nTest1的引用(由於C++的引用本身就是語法糖,反彙編層面看到的是指標,但是結合原始碼分析不難得出這裡應該是引用)。下面跟蹤進其建構函式一探究竟:

01181F20 55                   push        ebp  
01181F21 8B EC                mov         ebp,esp  
01181F23 81 EC CC 00 00 00    sub         esp,0CCh  
; 省略部分現場保護和堆疊填充int 3的程式碼
01181F40 89 4D F8             mov         dword ptr [this],ecx 
; 01181F40 89 4D F8             mov         dword ptr [ebp-8],ecx 
01181F43 8B 45 F8             mov         eax,dword ptr [this]  
; 01181F43 8B 45 F8             mov         eax,dword ptr [ebp-8]  
01181F46 8B 4D 08             mov         ecx,dword ptr [<nTest1>] 
; 01181F46 8B 4D 08             mov         ecx,dword ptr [ebp+8] 
01181F49 8B 11                mov         edx,dword ptr [ecx]  
01181F4B 89 10                mov         dword ptr [eax],edx  
01181F4D 8B 45 F8             mov         eax,dword ptr [this] 
; 01181F4D 8B 45 F8             mov         eax,dword ptr [ebp-8]
; 省略部分現場恢復程式碼
01181F53 8B E5                mov         esp,ebp  
01181F55 5D                   pop         ebp  
01181F56 C2 04 00             ret         4  

由此可以確定,lambda匿名類會將捕獲引數中的變數新增到其成員變數中,並設定一個帶有該引數引用型別的建構函式。最後再來看看在其operator()函式中是怎樣使用捕獲變數的:

    49:     auto f = [nTest1] (int a, int b) -> int
    50:     {
01183380 55                   push        ebp  
01183381 8B EC                mov         ebp,esp  
01183383 81 EC CC 00 00 00    sub         esp,0CCh  
; 省略部分現場保護和堆疊填充int 3的程式碼
011833A0 89 4D F8             mov         dword ptr [this],ecx  
    51:         return a + b + 42 + nTest1;
011833A3 8B 45 08             mov         eax,dword ptr [a]  
011833A6 03 45 0C             add         eax,dword ptr [b]  
011833A9 8B 4D F8             mov         ecx,dword ptr [this]  
011833AC 8B 11                mov         edx,dword ptr [ecx]  
011833AE 8D 44 10 2A          lea         eax,[eax+edx+2Ah]  
    52:     };
; 省略部分現場恢復程式碼
011833B5 8B E5                mov         esp,ebp  
011833B7 5D                   pop         ebp  
011833B8 C2 08 00             ret         8  

真相大白了,值捕獲時,C++編譯器在構建lambda表示式的匿名類時將區域性變數的引用傳入,並在建構函式中完成對相應成員變數的賦值。在呼叫其operator()函式時,如果用到了捕獲列表中的區域性變數,則從給匿名類物件的成員變數中取出。

引用捕獲

使用引用捕獲一個外部變數,需在捕獲列表變數前面加上一個引用說明符&。如下:

void fnTest()
{
    int nTest1 = 23;

    auto f = [&nTest1] (int a, int b) -> int
    {
        cout << "In functor before change nTest=" << nTest1 << endl;    //nTest1=23333
        nTest1 = 131;
        cout << "In functor after change nTest=" << nTest1 << endl;     // nTest1 = 131
        return a + b + 42 + nTest1;
    };

    nTest1 = 23333;     

    cout << f(4, 3) << "&nTest1=" << nTest1 << endl;        //nTest1 = 23333
}

這個結果有點詭異,按說傳遞引用並進行修改以後,結果應該會對外部的區域性變數產生影響,但這裡並沒有。反彙編看看什麼情況:

00C23C01 C7 45 E8 17 00 00 00 mov         dword ptr [nTest1],17h  
    48: 
    49:     auto f = [&nTest1] (int a, int b) -> int
    50:     {
    51:         cout << "In functor before change nTest=" << nTest1 << endl;    //nTest1=23333
    52:         nTest1 = 131;
    53:         cout << "In functor after change nTest=" << nTest1 << endl;     // nTest1 = 131
    54:         return a + b + 42 + nTest1;
    55:     };
00C23C08 8D 45 E8             lea         eax,[nTest1]  
00C23C0B 50                   push        eax  
00C23C0C 8D 4D DC             lea         ecx,[f]  
00C23C0F E8 0C E3 FF FF       call        <lambda_856c2229edde1846e85c005aaea059db>::<lambda_856c2229edde1846e85c005aaea059db> (0C21F20h)  
    56: 
    57:     nTest1 = 23333;     
00C23C14 C7 45 E8 25 5B 00 00 mov         dword ptr [nTest1],5B25h  
    58: 
    59:     cout << f(4, 3) << "&nTest1=" << nTest1 << endl;        //nTest1 = 23333
00C23C1B 8B F4                mov         esi,esp  
00C23C1D 68 DC 10 C2 00       push        offset std::endl<char,std::char_traits<char> > (0C210DCh)  
    58: 
    59:     cout << f(4, 3) << "&nTest1=" << nTest1 << endl;        //nTest1 = 23333
00C23C22 8B FC                mov         edi,esp  
00C23C24 8B 45 E8             mov         eax,dword ptr [nTest1]  
00C23C27 50                   push        eax  
00C23C28 68 90 BB C2 00       push        offset string "&nTest1=" (0C2BB90h)  
00C23C2D 6A 03                push        3  
00C23C2F 6A 04                push        4  
00C23C31 8D 4D DC             lea         ecx,[f]  
00C23C34 E8 B7 02 00 00       call        <lambda_856c2229edde1846e85c005aaea059db>::operator() (0C23EF0h)  

可以看到,lambda表示式匿名物件在建構函式中依然傳入了nTest的地址(引用),下面看看建構函式:

; 省略一大堆現場保護程式碼
00C21F40 89 4D F8             mov         dword ptr [this],ecx  
00C21F43 8B 45 F8             mov         eax,dword ptr [this]  
00C21F46 8B 4D 08             mov         ecx,dword ptr [<nTest1>]  
00C21F49 89 08                mov         dword ptr [eax],ecx  
00C21F4B 8B 45 F8             mov         eax,dword ptr [this]  
; 省略一大堆現場恢復程式碼

哈哈,注意這句 mov dword ptr [eax],ecx ,由於建構函式呼叫時,傳入的是nTest1的地址,這裡渠道地址後,直接存到了成員物件中。而在其operator()函式中,取出nTest1值的程式碼如下:

00C23F1A 8B 45 F8             mov         eax,dword ptr [this]  
00C23F1D 8B 08                mov         ecx,dword ptr [eax]  
00C23F1F 8B FC                mov         edi,esp  
00C23F21 8B 11                mov         edx,dword ptr [ecx]  

可以看到,是通過成員變數中的指標完成的。此外再看看對其賦值的操作:

    52:         nTest1 = 131;
00C23F55 8B 45 F8             mov         eax,dword ptr [this]  
00C23F58 8B 08                mov         ecx,dword ptr [eax]  
00C23F5A C7 01 83 00 00 00    mov         dword ptr [ecx],83h 

也是用指標完成的。其實這時nTest1的值已經被修改了。但是為什麼cout打印出來還是2333呢?這其實是由於cout時從由向左壓入引數的結果,再仔細看看:

00C23C24 8B 45 E8             mov         eax,dword ptr [nTest1]  
00C23C27 50                   push        eax                   ; eax=2333
00C23C28 68 90 BB C2 00       push        offset string "&nTest1=" (0C2BB90h)  
00C23C2D 6A 03                push        3  
00C23C2F 6A 04                push        4  

注意前兩句,已將呼叫lambda物件前的值壓棧了,則傳入的值當然是2333。但在cout語句執行後檢視nTest1的值,其實已經被修改為131了。

隱式捕獲

上面的值捕獲和引用捕獲都需要我們在捕獲列表中顯示列出Lambda表示式中使用的外部變數。除此之外,我們還可以讓編譯器根據函式體中的程式碼來推斷需要捕獲哪些變數,這種方式稱之為隱式捕獲。隱式捕獲有兩種方式,分別是[=]和[&]。[=]表示以值捕獲的方式捕獲外部變數,[&]表示以引用捕獲的方式捕獲外部變數。

下面將所有捕獲方式總結如下:

捕獲形式 說明
[] 不捕獲任何外部變數
[變數名, …] 預設以值得形式捕獲指定的多個外部變數(用逗號分隔),如果引用捕獲,需要顯示宣告(使用&說明符)
[this] 以值的形式捕獲this指標
[=] 以值的形式捕獲所有外部變數
[&] 以引用形式捕獲所有外部變數
[=, &x] 變數x以引用形式捕獲,其餘變數以傳值形式捕獲
[&, x] 變數x以值的形式捕獲,其餘變數以引用形式捕獲

相關推薦

C++lambda表示式原理分析

lambda表示式的本質就是過載了()運算子的類,這種類通常被稱為functor,即行為像函式的類。因此lambda表示式物件其實就是一個匿名的functor。 C++中lambda表示式的構成 一個標準的lambda表示式包括:捕獲列表、引數列表、mu

Javalambda表示式

原文地址:http://blog.laofu.online/2018/04/20/java-lambda/ 為什麼使用lambda 在java中我們很容易將一個變數賦值,比如int a =0;int b=a; 但是我們如何將一段程式碼和一個函式賦值給一個變數?這個變數應該是什麼的型別? 在java

Hive壓縮使用效能分析

     HIVE底層是hdfs和mapreduce實現儲存和計算的。所以HIVE可以使用hadoop自帶的InputFormat和Outputformat實現從不同的資料來源讀取檔案和寫出不同格式的檔案到檔案系統中。同理,HIVE也可以使用hadoop配置的壓縮方

Android之SharedPreferences原理分析

SharedPreferences作為Android儲存資料方式之一,主要特點是: 1. 只支援Java基本資料型別,不支援自定義資料型別; 2. 應用內資料共享; 3. 使用簡單. 使用方法 1、存資料 SharedPreferenc

C++Lambda表示式

我是搞C++的 一直都在提醒自己,我是搞C++的;但是當C++11出來這麼長時間了,我卻沒有跟著隊伍走,發現很對不起自己的身份,也還好,發現自己也有段時間沒有寫C++程式碼了。今天看到了C++中的Lambda表示式,雖然用過C#的,但是C++的,一直沒有用,也不知道怎麼

Java8新特性Stream APILambda表示式(1)

1 為什麼需要Stream與Lambda表示式? 1.1  為什麼需要Stream Stream作為 Java 8 的一大亮點,它與 java.io 包裡的 InputStream 和 OutputStream 是完全不同的概念。它也不同於 StAX 對 XML 解析的 S

C/C++extern關鍵字

編譯器 fin 生成 接口 bcd 只需要 c++環境 結束 編程 轉自:http://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777431.html 1 基本解釋:extern可以置於變量或者函數前,以標示變量或者

C/C++作用域(轉)

防止 局部作用域 gist 文件中 方式 為什麽不使用 形式參數 lan archive 作用域規則告訴我們一個變量的有效範圍,它在哪兒創建,在哪兒銷毀(也就是說超出了作用域)。變量的有效作用域從它的定義點開始,到和定義變量之前最鄰近的開括號配對的第一個閉括號。也就是說,作

C#const用法

htm 鏈接 服務器 span img body 用法詳解 -s 設計 本文實例講述了C#中const用法。分享給大家供大家參考。具體用法分析如下: const是一個c語言的關鍵字,它限定一個變量不允許被改變。使用const在一定程度上可以提高程序的安全性和可靠性,另外,

C#protected用法

base 而是 報錯 public 我們 此刻 訪問 .html 定義 轉自(https://www.cnblogs.com/wangyt223/archive/2012/08/08/2627801.html) 在c#的可訪問性級別中,public和private算是最

c/c++static的

extern info system pan 特點 靜態成員 額外 定義 全局 C 語言的 static 關鍵字有三種(具體來說是兩種)用途: 1. 靜態局部變量:用於函數體內部修飾變量,這種變量的生存期長於該函數。 int foo(){ st

C#ToString()格式

padding design otn href 有效 詳解 pattern console AS 以下內容均摘自博客園,僅供資料查詢。原文連接http://www.cnblogs.com/xdotnet/archive/2009/01/17/tostring_format.

C++的繼承

C++ 繼承 [TOC] 繼承基本知識 定義:  繼承是面向對復用的重要手段。通過繼承定義一個類,繼承是類型之間的關系建模,共享公有的東西,實現各自本質不同的東西。 繼承關系:  三種繼承關系下基類成員的在派生類的訪問關系變化(圖) 舉個栗子(公有繼承) ```c+

C++列舉enum

轉載部落格地址:https://blog.csdn.net/bruce_0712/article/details/54984371     眾所周知,C/C++語言可以使用#define和const建立符號常量,而使用enum工具不僅能夠建立符號常量,還能定義新

C/C++volatile關鍵字

asm 運行 多線程並發 這樣的 修改 由於 設定 其他 硬件 1. 為什麽用volatile? C/C++ 中的 volatile 關鍵字和 const 對應,用來修飾變量,通常用於建立語言級別的 memory barrier。這是 BS 在 "The C++ P

《隨筆十七》—— C++的 “ 指標

目錄 指標的概念 指標所指向的型別 指標的值 指標本身所佔據的記憶體區 指標的算術運算          運算子&和* 指標表示式 陣列和指標的關係 指標和結構型別的關係 指標和函式的關係

C++的string

標準庫型別string表示可變長的字元序列,為了在程式中使用string型別,我們必須包含標頭檔案: #include <string>  宣告一個字串 宣告一個字串有很多種方式,具體如下: 1 string s;//呼叫預設建構函式,s為一個空

C++string類(轉載)(最下面有程式碼實現)

作者:yzl_rex 來源:CSDN 原文:https://blog.csdn.net/yzl_rex/article/details/7839379 要想使用標準C++中string類,必須要包含 #include < string>// 注意是< string>

C++const關鍵字

const關鍵字作用    1. 修飾變數        用法:const 型別說明符 變數名。        含義:說明該變數不可以被改變。        用途:常量命名等    2. 修飾

quartz定時表示式

(一)格式講解 Cron表示式的格式:秒 分 時 日 月 周 年。其欄位取值如下圖所示: “?”字元:表示不確定的值 “,”字元 :指定數個值 “-”字元:指定一個值的範圍 “/”字元:指定一個值的增加幅度。n/m表示從n開始,每次增加m “L”