到了C++11最激動人心的特性了:
匿名函數:lambda表達式
假設你有一個vector<int> v, 想知道里面大於4的數有多少個。for循環誰都會寫,但是STL提供了現成算法count_if,不用可惜。C++03時代,我們會這樣寫:
#include <iostream> #include <vector> #include <algorithm> using namespace std; bool gt4(int x){ return x>4; } struct GT4{ bool operator()(int x){ return x>4; } }; int main(){ vector<int> v;
很多v.push_back(...);
... cout<<count_if(v.begin(),v.end(),gt4)<<endl; cout<<count_if(v.begin(),v.end(),GT4())<<endl; }
就為這樣一個微功能,要么寫一個函數,要么寫個仿函數functor,還不如手寫循環簡單,這是我的感受。如果用過其他語言的lambda表達式,這種寫法完全是渣渣。
C++引入的lambda表達式提供了一種臨時定義匿名函數的方法,可以這樣寫:
int res = count_if(v.begin(),v.end(),[](int x){ return x>4; });
世界瞬間美好了。既然是匿名函數,函數名自然不用寫了,連返回類型都不用寫了~想用一個函數,用的時候再寫,大大提高了algorithm里各種泛型算法的實用性。
一般的lambda表達式語法是
[捕獲列表] (參數列表) -> 返回類型 {函數體}
->返回類型可以省略;如果是無參的,(參數列表)也可以省略,真是各種省。匿名函數是個lambda對象,和函數指針有區別,但一般不用關心它。如果你想把一個匿名函數賦給一個函數指針類似物以待后續使用,可以用auto
auto func = [](int arg) { ... };
但捕獲列表是什么?接下來:
閉包closure
如果改主意了,要求>k的個數,k運行時指定,怎么辦?你可能會寫
int k; cin>>k; int res = count_if(v.begin(),v.end(),[](int x){ return x>k; }); //WRONG!
但是編譯器報錯:
error: variable 'k' cannot be implicitly captured in a lambda with no capture-default specified return x>k;
匿名函數不能訪問外部函數作用域的變量?太弱了!
如果真是這樣,實用性的確有限。lambda的捕獲列表就是指定你要訪問哪些外部變量,這里是k,於是
int res = count_if(v.begin(),v.end(),[k](int x){ //注意[]里的 return x>k; }); //OK!
如果要捕獲多個變量,可以用逗號隔開。如果要捕獲很多變量,干脆一起打包算了,用'='捕獲所有:
int res = count_if(v.begin(),v.end(),[=](int x){ return x>k; }); //OK, too!
通俗的說:子函數可以使用父函數中的局部變量,這種行為就叫做閉包。
解釋一下各種捕獲方式:
捕獲capture有些類似傳參。使用[k], [=]聲明的捕獲方式是復制copy,類似傳值。區別在於,函數參數傳值時,對參數的修改不影響外部變量,但copy的捕獲直接禁止你去修改。如果想修改,可以使用引用方式捕獲,語法是[&k]或[&]。引用和復制可以混用,如
int i,j; [=i, &j] (){...};
但閉包的能力遠不止“使用外部變量”這么簡單,最奇幻的是它可以超越傳統C++對變量作用域生存期的限制。我們嘗試一些刺激的。
假設你要寫一個等差數列生成器,初值公差運行時指定,行為和函數類似,第k次調用生成第k個值,並且各個生成器互不干擾,怎么寫?
普通函數不好優雅地保存狀態(全局變量無力了吧)。用仿函數好了,成員變量保存每個計數器的狀態:
struct Counter { int cur; int step; Counter(int _init,int _step){ cur = _init; step = _step; } int operator()(){ cur = cur+step; return cur-step; } }; int main(){ Counter c(1,3); for(int i=0;i<4;++i){ cout<<c()<<endl; } //輸出1 4 7 10 }
但是我們現在有了閉包!把狀態作為父函數中的局部變量,各個counter就可以不影響了。由於要修改外部變量,根據之前的介紹,聲明成引用捕獲[&]。寫起來大體像這樣:
??? Counter(int init,int step){ int cur = init; return [&]{ cur += step; return cur-step; } } int main(){ auto c = Counter(1,3); for(int i=0;i<4;++i){ cout<<c()<<endl; } }
兩個問題!
第一個:Counter函數的返回類型怎么寫???
Counter返回值是一個lambda,賦給c時可以用auto騙過去,但聲明時寫類型是躲不過去了。返回類型后置+decltype救不了你,因為后置了decltype還是獲取不到返回值類型。lambda對象,雖然行為像函數指針,但是不能直接賦給一個函數指針。
介紹一個C++11新的模板類function,是消滅丑陋函數指針的大殺器。你可以把一個函數指針或lambda賦給它,例如
#include <functional>
int func(float a,float b) {
return a+b;
} function<int(float,float)> pfunc = func; function<int(float,float)> plambda = [](float a,float b){ return a+b;};
比函數指針好看多了。
於是這里可以寫:
function<int()> Counter(int init,int step){ ... }
但是!如果再瘋狂一點,匿名函數可以省略返回類型,auto可以推導類型,結合起來這樣寫是可以的!
auto Counter = [](int init,int step){ int cur = init; return [&](){ cur += step; return cur-step; }; }; //不要漏';' 根本上還是賦值語句
“類型推導, auto和decltype”一節里留的trick就是這個。javascript的即視感有木有!
第二個:編譯通過,運行輸出是這個???
1 167772160 167772160 167772160
看起來像是訪問了無效內存。的確是這樣。cur,step這兩個局部變量在父函數的棧幀中,內部的匿名函數返回以后,父函數的棧幀就銷毀了,而我們用的是“引用”,引用的變量已經沒了。
既然放在棧上會有生存期問題,那就放堆里
auto Counter = [](int init,int step){ int* pcur = new int(init); int* pstep = new int(step); return [=](){ //注意!&變成了= *pcur += *pstep; return *pcur-*pstep; }; };
注意使用了[=]而不是[&]。解釋:
- 我們沒有直接修改捕獲的指針變量,而是修改它指向的變量,和[=]的規則不沖突
- 外部的指針還是在棧上,如果用[&]還是會引用到已銷毀的指針。我們只需要復制一份指針值。
這樣輸出的確正常了,但是內存泄漏了。程序員的節操呢?
用智能指針可以解決內存泄漏:
auto Counter = [](int init,int step){ shared_ptr<int> pcur(new int(init)); shared_ptr<int> pstep(new int(step)); return [=](){ *pcur += *pstep; return *pcur-*pstep; }; };
雖然解決了問題,但過於繁瑣了。本質上,我們需要的效果是把父函數的局部變量生存期延長,至少和子函數一樣長。C++11提供了mutable關鍵字,可以模擬這一功能:
auto Counter = [](int init,int step){ int cur = init; return [=] () mutable { cur += step; return cur-step; }; };
加上mutable,就告訴編譯器,這個變量是父子函數共享的,子函數對它的修改要反映到外部,並且它的生存期要和子函數一樣長!
這里可能有點繞,函數哪來的生存期?注意這里“子函數”並不是真正的函數,只是一個lambda類型的變量,只是有函數的行為,一樣有生存期。
閉包最大的用處在於寫回調函數,比如事件響應。當初學Java的時候,Swing里用戶界面各種內部類,感覺很煩。現在Java終於也有閉包了(Java8)~