遇見C++ Lambda

來源:互聯網
上載者:User

遇見C++ Lambda

 

Written by Allen Lee

 

If you die when there's no one watching, and your ratings drop and you're forgotten.

– Marilyn Manson, Lamb Of God

 

產生隨機數字

      假設我們有一個vector<int>容器,想用100以內的隨機數初始化它,其中一個辦法是通過generate函數產生,如代碼1所示。generate函數接受三個參數,前兩個參數指定容器的起止位置,後一個參數指定產生邏輯,這個邏輯正是通過Lambda來表達的。

代碼 1

      我們現在看到Lambda是最簡形式,只包含捕獲子句和函數體兩個必要部分,其他部分都省略了。[]是Lambda的捕獲子句,也是引出Lambda的文法,當編譯器看到這個符號時,就知道我們在寫一個Lambda了。函數體通過{} 包圍起來,裡面的代碼和一個普通函數的函數體沒有什麼不同。

      那麼,代碼1產生的隨機數字裡有多少個奇數呢,我們可以通過for_each函數數一下,如代碼3所示。和generate函數不同的是,for_each函數要求我們提供的Lambda接受一個參數。一般情況下,如果Lambda的參數列表不包含任何參數,我們可以把它省略,就像代碼1所示的那樣;如果包含多個參數,可以通過逗號分隔,如(int index, std::string item)。

代碼 2

      看到這裡,細心的讀者可能已經發現代碼2的捕獲子句裡面多了一個"&odd_count",這是用來幹嘛的呢?我們知道,這個代碼的關鍵區段是在Lambda的函數體裡修改一個外部的計數變數,常見的語言(如C#)會自動為Lambda捕獲當前內容相關的所有變數,但C++要求我們在Lambda的捕獲子句裡顯式指定想要捕獲的變數,否則無法在函數體裡使用這些變數。如果捕獲子句裡面什麼都不寫,像代碼1所示的那樣,編譯器會認為我們不需要捕獲任何變數。

      除了顯式指定想要捕獲的變數,C++還要求我們指定這些變數的傳遞方式,可以選擇的傳遞方式有兩種:按值傳遞和按引用傳遞。像[&odd_count] 這種寫法是按引用傳遞,這種傳遞方式使得你可以在Lambda的函數體裡對odd_count變數進行修改。相對的,如果變數名字前面沒有加上"&"就是按值傳遞,這些變數在Lambda的函數體裡是唯讀。

      如果你希望按引用傳遞捕獲當前內容相關的所有變數,可以把捕獲子句寫成[&];如果你希望按值傳遞捕獲當前內容相關的所有變數,可以把捕獲子句寫成[=]。如果你希望把按引用傳遞設為預設的傳遞方式,同時指定個別變數按值傳遞,可以把捕獲子句寫成[&, a, b];同理;如果預設的傳遞方式是按值傳遞,個別變數按引用傳遞,可以把捕獲子句寫成[=, &a, &b]。值得提醒的是,像[&, a, &b]和[=, &a, b]這些寫法是無效的,因為預設的傳遞方式均已覆蓋b變數,無需單獨指定,有效寫法應該是[&, a]和[=, &a]。

 

產生等差數列

      現在我們把一開始的問題改一下,通過generate函數產生一個首項為0,公差為2的等差數列。有了前面關於捕獲子句的知識,我們很容易想到代碼3這個方案,首先按引用傳遞捕獲i變數,然後在Lambda的函數體裡修改它的值,並返回給generate函數。

代碼 3

      如果我們把i變數的傳遞方式改成按值傳遞,然後在捕獲子句後面加上mutable聲明,如代碼4所示,我們可以得到相同的效果,我指的是輸出結果。那麼,這兩個方案有什麼不一樣呢?調用generate函數之後檢查一下i變數的值就會找到答案了。需要說明的是,如果我們加上mutable聲明,參數列表就不能省略了,即使裡面沒有包含任何參數。

代碼 4

      使用代碼3這個方案,i變數的值在調用generate函數之後是18,而使用代碼4這個方案,i變數的值是-2。這個意味著mutable聲明使得我們可以在Lambda的函數體修改按值傳遞的變數,但這些修改對Lambda以外的世界是不可見的,有趣的是,這些修改在Lambda的多次調用之間是共用的。換句話說,代碼4的generate函數調用了10次Lambda,前一次調用時對i變數的修改結果可以在後一次調用時訪問得到。

      這聽起來就像有個對象,i變數是它的成員欄位,而Lambda則是它的成員函數,事實上,Lambda是函數對象(Function Object)的文法糖,代碼4的Lambda最終會被轉換成代碼5所示的Functor類。

代碼 5

你也可以把代碼4的Lambda替換成Functor類,如代碼6所示。

代碼 6

 

如何聲明Lambda的類型?

      到目前為止,我們都是把Lambda作為參數直接傳給函數的,如果我們想把一個Lambda傳給多個函數,或者把它當作一個函數多次調用,那麼就得考慮把它存到一個變數裡了,問題是這個變數應該如何聲明呢?如果你確實不知道,也不想知道,那麼最簡單的辦法就是交給編譯器處理,如代碼7所示,這裡的auto關鍵字相當於C#的var,編譯器會根據我們用來初始化f1變數的值推斷它的實際類型,這個過程是靜態,在編譯時間完成。

代碼 7

      如果我們想定義一個接受代碼7的Lambda作為參數的函數,那麼這個參數的類型又該如何寫呢?我們可以把它聲明為function模板類型,如代碼8所示,裡面的型別參數反映了Lambda的簽名——兩個int參數,一個int傳回值。

代碼 8

此外,你也可以把這個函式宣告為模板函數,如代碼9所示。

代碼 9

無論你如何聲明這個函數,調用的時候都是一樣的,而且它們都能接受Lambda或者函數對象作為參數,如代碼10所示。

代碼 10

 

捕獲變數的值什麼時候確定?

      現在,我要把代碼7的Lambda調整成代碼11所示的那樣,通過捕獲子句而不是參數列表提供輸入,這兩個參數分別使用不同的傳遞方式,那麼,我在第三行修改這兩個參數的值會否對第四行的調用產生影響?

代碼 11

      如果你運行代碼11,你將會看到輸出結果是5。為什嗎?這是因為按值傳遞在聲明Lambda的那一刻就已經確定變數的值了,無論之後外面怎麼修改,裡面只能訪問到聲明時傳過來的版本;而按引用傳遞則剛好相反,裡面和外面看到的是同一個東西,因此在調用Lambda之前外面的任何修改對裡面都是可見的。這種問題在C#裡是沒有的,因為C#只有按引用傳遞這種方式。

 

傳回值的類型什麼時候可以省略?

      最後,我們一直沒有提到傳回值的類型,編譯器會一直幫我們自動推斷嗎?不會,只有兩種情況可以在聲明Lambda時省略傳回值類型,而前面的例子剛好都滿足這兩種情況,因此推到現在才說:

  • 函數體只包含一條返回語句,如最初的代碼1所示。
  • Lambda沒有傳回值,如代碼2所示。

當你需要加上傳回值的類型時,必須把它放在參數列表後面,並且在傳回值類型前面加上"->"符號,如代碼12所示。

代碼 12

 

*以上代碼均在Visual Studio 2010和Visual Studio 2012 RC上測試通過。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.