C++11 引入了lambda表達式,這個特性的最普遍應用,就是配合泛型算法。
泛型算法,采用了迭代器操作,從而使得各種不同的容器能使用一套算法。泛型算法允許我們定制自己的操作,即傳遞一個可調用對象,lambda其實也是一個可調用對象。下面先介紹下lambda表達式的基本結構和特性,最后再深入探索lambda表達式的本質。
一、lambda表達式的基本構成
lambda表達式表示了一個可調用的單元代碼,與函數類似,具有一個返回類型,一個參數列表,一個函數體,不同之處在於,還有一個捕獲參數列表。
[](int a, int b)->bool{return a > b; }
需要注意的是,返回類型必須使用尾置返回。參數列表和返回類型都是可以忽略的,但是捕獲列表和函數體必須保留。個人猜想,省略了捕獲列表和函數體可能會使得編譯器無法判斷類型。
lambda既然是個可調用對象,那么它的調用方法也與一般的函數對象一致:
class Increase { public: void operator()(int& val){++val;} }; int main() { int i = 1, j = 1; auto f = [](int& j){ ++j; };//等價於Increase f(j);//1 Increase Inc; Inc(i);//2 }//1與2等價
二、捕獲
捕獲可以說是lambda最難理解的部分。所謂捕獲,指的是允許lambda使用它所在函數中的局部變量。對於函數外的變量以及函數內的static變量,無需進行捕獲即可直接使用。
捕獲有兩種方式,一種是值捕獲,另外一種是引用捕獲。
static i=1; int j=1; auto f1 = [&j]()->int{ ++j; return j; };//引用捕獲 auto f2 = [j](){return j; };//值捕獲 auto f3 = [](){return i; }//靜態變量無需捕獲
引用捕獲,實際上就是引用了一個局部變量。既然是引用,就要注意引用存在的問題。必須確保引用的變量在整個lambda執行的周期內都是存在的,並且為一個正確的值。
值捕獲,相當於函數參數值傳遞的過程。因此,必須保證捕獲的量是可拷貝的。此外,針對捕獲的值為指針或者引用類型,也存在確保對象依然存在的問題。
因此,應對盡量減少捕獲,避免潛在的問題。可能的話,盡量避免捕獲指針或者引用。
隱式捕獲
隱式捕獲是一種簡略的方法,下面舉個例子來說明一下:
int i = 1, j = 1; auto f = [=, &j](){return i + (++j); };
前面“=”或者“&”說明默認捕獲的類型,如果有需要顯示捕獲的另外一種類型,在后面單獨列出即可。
修改捕獲量的值
auto f2 = [j](){return ++j; };//error,因為這是個右值 auto f4 = [j]()mutable{return ++j; };//正確
值捕獲是無法修改變量值的,加一個mutable即可。
三、返回類型
前面的例子中我們可以看到,我們沒有顯示說明返回類型,也得到了正確的結果。不過,默認的規則如下
1、如果僅有return ,那么根據return 的類型確定返回類型。
2、如果除了return 還有別的語句,那么返回void。
所以,不僅有return語句時,盡量要自己顯示說明返回類型。
四、lambda表達式的本質
我們知道,定義了調用運算符的對象,可以稱為函數對象,即這個對象可以像函數一樣被調用,行為類似函數。
與lambda一樣,函數對象同樣可以作為泛型算法的自定義操作:
class Bigger//1 { public: bool operator()(int a, int b){ return a > b; } }; bool bigger(int a, int b){ return a > b; } int main() { vector<int> vec{ 2, 0, 1, 3, 3, 0, 1, 9, 7, 7 }; sort(vec.begin(), vec.end(), [](int a, int b)->bool{return a > b; });//2 sort(vec.begin(), vec.end(), bigger); sort(vec.begin(), vec.end(), Bigger()); }
上面的例子中,分別用了lambda表達式、函數指針、函數對象來進行了排序,結果是相同的。
事實上,lambda表達式就是一個函數對象。當編寫了一個lambda表達式的時候,編譯器將該表達式翻譯成一個未命名類的未命名對象。
lambda表達式產生的類中會有一個重載的函數調用運算符。如果沒有捕獲任何變量,如上面我們定義的lambda(1)和函數對象(2),其實是完全相同的。
如果lambda采用引用捕獲的方式,那么該變量其實不是lambda對象的成員,所以無需進行額外操作。
如果采用值捕獲,那么就相當於在對象內部創建了自己的成員,因此需要增加一部分內容:
class F { public: F(int n) :num(n){} int operator()(){ return num; } private: int num; }; int num = 100; auto f = [num](){return num; };//等價於F
即增加了一個用捕獲的值進行構造的構造函數。
lambda表達式產生的類,不含有默認構造函數、賦值運算符和默認析構函數,其他成員由需要捕獲的類型確定。
本文只是簡單介紹了lambda的基本操作和功能,以及lambda表達式編譯時產生函數對象的行為,更加深入的內容,本人暫時還不清楚0 0