遇見C++ Lambda
生成隨機(jī)數(shù)字
假設(shè)我們有一個(gè)vector<int>容器,想用100以內(nèi)的隨機(jī)數(shù)初始化它,其中一個(gè)辦法是通過generate函數(shù)生成,如代碼1所示。 generate函數(shù)接受三個(gè)參數(shù),前兩個(gè)參數(shù)指定容器的起止位置,后一個(gè)參數(shù)指定生成邏輯,這個(gè)邏輯正是通過Lambda來表達(dá)的。
代碼 1
我們現(xiàn)在看到Lambda是最簡(jiǎn)形式,只包含捕獲子句和函數(shù)體兩個(gè)必要部分,其他部分都省略了。[]是Lambda的捕獲子句,也是引出Lambda的語法,當(dāng)編譯器看到這個(gè)符號(hào)時(shí),就知道我們?cè)趯懸粋€(gè)Lambda了。函數(shù)體通過{} 包圍起來,里面的代碼和一個(gè)普通函數(shù)的函數(shù)體沒有什么不同。
那么,代碼1生成的隨機(jī)數(shù)字里有多少個(gè)奇數(shù)呢,我們可以通過for_each函數(shù)數(shù)一下,如代碼3所示。和generate函數(shù)不同的是,for_each函數(shù)要求我們提供的Lambda接受一個(gè)參數(shù)。一般情況下,如果Lambda的參數(shù)列表不包含任何參數(shù),我們可以把它省略,就像代碼 1所示的那樣;如果包含多個(gè)參數(shù),可以通過逗號(hào)分隔,如(int index, std::string item)。
代碼 2
看到這里,細(xì)心的讀者可能已經(jīng)發(fā)現(xiàn)代碼2的捕獲子句里面多了一個(gè)"&odd_count",這是用來干嘛的呢?我們知道,這個(gè)代碼的關(guān)鍵部分是在 Lambda的函數(shù)體里修改一個(gè)外部的計(jì)數(shù)變量,常見的語言(如C#)會(huì)自動(dòng)為L(zhǎng)ambda捕獲當(dāng)前上下文的所有變量,但C++要求我們?cè)贚ambda的捕獲子句里顯式指定想要捕獲的變量,否則無法在函數(shù)體里使用這些變量。如果捕獲子句里面什么都不寫,像代碼1所示的那樣,編譯器會(huì)認(rèn)為我們不需要捕獲任何變量。
除了顯式指定想要捕獲的變量,C++還要求我們指定這些變量的傳遞方式,可以選擇的傳遞方式有兩種:按值傳遞和按引用傳遞。像 [&odd_count] 這種寫法是按引用傳遞,這種傳遞方式使得你可以在Lambda的函數(shù)體里對(duì)odd_count變量進(jìn)行修改。相對(duì)的,如果變量名字前面沒有加上"&"就是按值傳遞,這些變量在Lambda的函數(shù)體里是只讀的。
如果你希望按引用傳遞捕獲當(dāng)前上下文的所有變量,可以把捕獲子句寫成[&];如果你希望按值傳遞捕獲當(dāng)前上下文的所有變量,可以把捕獲子句寫成 [=]。如果你希望把按引用傳遞設(shè)為默認(rèn)的傳遞方式,同時(shí)指定個(gè)別變量按值傳遞,可以把捕獲子句寫成[&, a, b];同理;如果默認(rèn)的傳遞方式是按值傳遞,個(gè)別變量按引用傳遞,可以把捕獲子句寫成[=, &a, &b]。值得提醒的是,像[&, a, &b]和[=, &a, b]這些寫法是無效的,因?yàn)槟J(rèn)的傳遞方式均已覆蓋b變量,無需單獨(dú)指定,有效的寫法應(yīng)該是[&, a]和[=, &a]。
生成等差數(shù)列
現(xiàn)在我們把一開始的問題改一下,通過generate函數(shù)生成一個(gè)首項(xiàng)為0,公差為2的等差數(shù)列。有了前面關(guān)于捕獲子句的知識(shí),我們很容易想到代碼3這個(gè)方案,首先按引用傳遞捕獲i變量,然后在Lambda的函數(shù)體里修改它的值,并返回給generate函數(shù)。
代碼 3
如果我們把i變量的傳遞方式改成按值傳遞,然后在捕獲子句后面加上mutable聲明,如代碼4所示,我們可以得到相同的效果,我指的是輸出結(jié)果。那么,這兩個(gè)方案有什么不一樣呢?調(diào)用generate函數(shù)之后檢查一下i變量的值就會(huì)找到答案了。需要說明的是,如果我們加上mutable聲明,參數(shù)列表就不能省略了,即使里面沒有包含任何參數(shù)。
代碼 4
使用代碼3這個(gè)方案,i變量的值在調(diào)用generate函數(shù)之后是18,而使用代碼4這個(gè)方案,i變量的值是-2。這個(gè)意味著mutable聲明使得我們可以在Lambda的函數(shù)體修改按值傳遞的變量,但這些修改對(duì)Lambda以外的世界是不可見的,有趣的是,這些修改在Lambda的多次調(diào)用之間是共享的。換句話說,代碼4的generate函數(shù)調(diào)用了10次Lambda,前一次調(diào)用時(shí)對(duì)i變量的修改結(jié)果可以在后一次調(diào)用時(shí)訪問得到。
這聽起來就像有個(gè)對(duì)象,i變量是它的成員字段,而Lambda則是它的成員函數(shù),事實(shí)上,Lambda是函數(shù)對(duì)象(Function Object)的語法糖,代碼4的Lambda最終會(huì)被轉(zhuǎn)換成代碼5所示的Functor類。
代碼 5
你也可以把代碼4的Lambda替換成Functor類,如代碼6所示。
代碼 6
如何聲明Lambda的類型?
到目前為止,我們都是把Lambda作為參數(shù)直接傳給函數(shù)的,如果我們想把一個(gè)Lambda傳給多個(gè)函數(shù),或者把它當(dāng)作一個(gè)函數(shù)多次調(diào)用,那么就得考慮把它存到一個(gè)變量里了,問題是這個(gè)變量應(yīng)該如何聲明呢?如果你確實(shí)不知道,也不想知道,那么最簡(jiǎn)單的辦法就是交給編譯器處理,如代碼7所示,這里的auto 關(guān)鍵字相當(dāng)于C#的var,編譯器會(huì)根據(jù)我們用來初始化f1變量的值推斷它的實(shí)際類型,這個(gè)過程是靜態(tài)的,在編譯時(shí)完成。
代碼 7
如果我們想定義一個(gè)接受代碼7的Lambda作為參數(shù)的函數(shù),那么這個(gè)參數(shù)的類型又該如何寫呢?我們可以把它聲明為function模板類型,如代碼8所示,里面的類型參數(shù)反映了Lambda的簽名——兩個(gè)int參數(shù),一個(gè)int返回值。
代碼 8
此外,你也可以把這個(gè)函數(shù)聲明為模板函數(shù),如代碼9所示。
代碼 9
無論你如何聲明這個(gè)函數(shù),調(diào)用的時(shí)候都是一樣的,而且它們都能接受Lambda或者函數(shù)對(duì)象作為參數(shù),如代碼10所示。
代碼 10
捕獲變量的值什么時(shí)候確定?
現(xiàn)在,我要把代碼7的Lambda調(diào)整成代碼11所示的那樣,通過捕獲子句而不是參數(shù)列表提供輸入,這兩個(gè)參數(shù)分別使用不同的傳遞方式,那么,我在第三行修改這兩個(gè)參數(shù)的值會(huì)否對(duì)第四行的調(diào)用產(chǎn)生影響?
代碼 11
如果你運(yùn)行代碼11,你將會(huì)看到輸出結(jié)果是5。為什么?這是因?yàn)榘粗祩鬟f在聲明Lambda的那一刻就已經(jīng)確定變量的值了,無論之后外面怎么修改,里面只能訪問到聲明時(shí)傳過來的版本;而按引用傳遞則剛好相反,里面和外面看到的是同一個(gè)東西,因此在調(diào)用Lambda之前外面的任何修改對(duì)里面都是可見的。這種問題在C#里是沒有的,因?yàn)镃#只有按引用傳遞這種方式。
返回值的類型什么時(shí)候可以省略?
最后,我們一直沒有提到返回值的類型,編譯器會(huì)一直幫我們自動(dòng)推斷嗎?不會(huì),只有兩種情況可以在聲明Lambda時(shí)省略返回值類型,而前面的例子剛好都滿足這兩種情況,因此推到現(xiàn)在才說:
- 函數(shù)體只包含一條返回語句,如最初的代碼1所示。
- Lambda沒有返回值,如代碼2所示。
當(dāng)你需要加上返回值的類型時(shí),必須把它放在參數(shù)列表后面,并且在返回值類型前面加上"->"符號(hào),如代碼12所示。
代碼 12
*以上代碼均在Visual Studio 2010和Visual Studio 2012 RC上測(cè)試通過。
原文鏈接:http://www.cnblogs.com/allenlooplee/archive/2012/07/03/2574119.html