1. 程式人生 > >(C/C++學習心得)8.C++ Lambda

(C/C++學習心得)8.C++ Lambda

一.生成隨機數字

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

程式碼1:

  1     vector<int> vec(10);
  2     generate(vec.begin(),vec.end(),[]{ return rand() % 100; });

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

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

程式碼2:

  1     int odd_count = 0;
  2     for_each(vec.begin(),vec.end(),[&odd_count](int value)
  3     {
  4
if(value %2 == 1) 5 odd_count++; 6 });

看到這裡,細心的讀者可能已經發現程式碼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:

  1     int step = 2;
  2     int i = 0;
  3     vector<int> vec(10);
  4     generate(vec.begin(),vec.end(),[&i,step]{ return (i += step); });
  5     for(int it:vec)

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

程式碼4:

  1     int step = 2;
  2     int i = 0;
  3     vector<int> vec(10);
  4     generate(vec.begin(),vec.end(),[i,step] () mutable { return (i += step); });
  5     cout<<i<<endl;

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

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

程式碼5:

  1 class functor
  2 {
  3 public:
  4     functor(int i,int step)
  5         :_i(i),_step(step){}
  6     int operator()()
  7     {
  8         return (_i += _step);
  9     }
 10 private:
 11     int _i;
 12     int _step;
 13 };

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

程式碼6:

  1     int i = 0,step = 2;
  2     vector<int> seq(10);
  3     generate(seq.begin(),seq.end(),functor(i,step));

三.如何宣告Lambda的型別?

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

程式碼7:

  1 auto f = [](int x,int y){ return x+y; };

如果我們想定義一個接受程式碼7的Lambda作為引數的函式,那麼這個引數的型別又該如何寫呢?我們可以把它宣告為function模板型別,如程式碼8所示,裡面的型別引數反映了Lambda的簽名——兩個int引數,一個int返回值。需要注意的是:function是標準庫裡面提供的一個模板型別,位於std名稱空間,使用之前需要#include <functional>。

程式碼8:

  1 void foo(function < int (int,int) > f)
  2 {
  3     cout<<f(1,2)<<endl;
  4 }

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

程式碼9:

  1 template<class Fn>
  2 void bar(Fn f)
  3 {
  4      cout<<f(1,2)<<endl;
  5 }

無論你如何宣告這個函式,呼叫的時候都是一樣的,而且它們都能接受Lambda或者函式物件作為引數,如程式碼10所示。

程式碼10:

  1 class functor
  2 {
  3 public:
  4     int operator()(int x,int y)
  5     {
  6         return (x+y);
  7     }
  8 };
  9 int main()
 10 {
 11     auto f = [](int x,int y){ return x+y; };
 12     foo(f);
 13     bar(f);
 14     foo(functor());
 15     bar(functor());
 16     return 0;
 17 }

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

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

程式碼11:

  1     int x = 1,y = 2;
  2     auto f = [x,&y] {return (x+y); };
  3     x = 3,y = 4;
  4     cout<<f()<<endl;

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

五.返回值的型別什麼時候可以省略?

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

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

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

程式碼12:

  1     int x = 1,y = 2;
  2     auto f1 = [x,y]()->int
  3     {
  4         return x+y;
  5     };
  6     cout<<f1()<<endl;
  7 }