這是一個關於C\C++程式員的一個小故事,關於C++11――剛剛通過的新標準的一個小故事… 請不要誤會,題目中所提及的“最佳化”並不是提升程式的效能――Lambda運算式幹不了這個。從本質上來說,它只是一種“文法糖”而已。不使用這種運算式,我們照樣可以寫出滿足需求的程式。正如放棄C而使用彙編,或者放棄彙編而使用機器語言一樣,你能控制的範圍就在那裡,不增不減。但如果有得選擇,我相信大部分人會選擇彙編而非機器語言,選擇C而非彙編,甚至選擇C++而非C語言……。如果你確實是這樣選擇的,那麼我有理由相信,你會選擇C++新標準中的Lambda運算式,因為它確實能夠簡化你的程式,讓你寫起程式來更容易;讓你的程式更易讀,更優美;同時也讓你有更多向同行炫耀的資本。 從一個實際的應用說起 讓我們還是看一個例子吧。 無論是C語言的使用者,還是C++的使用者,如果你從事PC程式的演算法開發,我有96.57%的把握認為你可能使用過C++標準模板庫STL(其中的string,vector之類)。畢竟,STL的抽象不錯,不用白不用,是不是。STL中有一大類是演算法,這些演算法的抽象同樣不錯,我們就拿排序演算法(sort)來說事吧。 假設現在有一個結構稱為Student,其中包含了ID與name兩項――分別表示學號與姓名。在某個應用中,使用者希望對一個Student的數組按照ID的從大到小排序,那麼程式可能寫成如下的形式(本文中的所有程式均在Visual Studio 2010下編譯通過): #include <string> #include <vector> #include <iostream> #include <iterator> #include <algorithm> using namespace std; struct Student { unsigned ID; string name; Student(unsigned i, string n) : ID(i), name(n){} }; struct compareID { bool operator ()(const Student& val1, const Student& val2)? ???const { return val1.ID < val2.ID; } }; int main(int argc, char* argv[]) { Student a[] = {Student(2, “John”), Student(0, “Tom”), Student(1, “Lily”)}; sort(a, a+3, compareID()); for(int i=0; i<3; ++i) cout<<a[i].ID<<’ ‘<<a[i].name<<endl; return 0 } 程式用sort進行排序,之後用一個for迴圈輸出結果。而之所以能完成這個排序,則是由於仿函數compardID的存在。 現在假設使用者的需求變了(或者是另一個需求),需要你按照學生的姓名進行排序,那麼你需要重新寫一個仿函數如下: struct compareName { bool operator ()(const Student& val1, const Student& val2) const? ? { return val1.name < val2.name; } }; 然後將sort的調用修改為: sort(a, a+3, compareName()); 問題出現了,你意識到了嗎?你只是想表達一個很簡單的排序方式,確不得不引入很多的程式碼來建相應的仿函數。如果這個函數在很多地方都會用到,那麼建立它的價值還相對較大。如果只是用在一個地方,你也不得不中段你流暢是思路,一邊罵娘一邊寫出這麼多行代碼。另一方面,程式的讀者在讀到相應部分的時候,也不得不中段他流暢的思路,在工程的某個地方苦苦求索――compareName或者compareID是怎麼乾的呢? 是的,是的,作為一個C++老鳥,你會說,這樣寫代碼太不專業了。完全可以有不建立仿函數的寫法,比如以ID排序時,完全可以通過引入boost庫中的bind來實現,比如這樣: sort(a, a+3, bind(less<unsigned>(), bind(&Student::ID, _1), bind(&Student::ID, _2))); 如果你能寫出或是讀懂這段代碼,我承認你的C++水平確實說得過去(如果讀不懂,沒關係,它不是本文的重點)。但這段代碼真的好嗎?確實,這樣可以省略了仿函數。但問題是代碼的複雜性大大增加了――即使如此簡單的一個需求,bind運算式也要複雜如斯,更複雜一點的需求要寫成何等複雜的形式啊,這對於bind本身,寫程式的人,讀程式的人都是一種折磨――你hold住嗎? 如果用Lambda運算式呢,唔,這個sort語句可以這麼寫: sort(a, a+3, [](const Student& val1, const Student& val2){ return val1.ID < val2.ID; }); 那個看上去有點奇怪的,sort的第三個函數就是一個Lambda運算式了。如果我們除去開頭的“[]”不看,後面的部分很像一個函數――你可以很容易地看出這個函數是幹什麼的:給定兩個Student元素,比較兩個元素的ID值,並返回比較結果――這玩意兒比上面那個bind結果容易閱讀多了。 事實上,利用Lambda運算式,上述程式可以修改為如下的樣子(只列出了main函數): int main(int argc, char* argv[]) { Student a[] = {Student(2, “John”), Student(0, “Tom”), Student(1, “Lily”)}; sort(a, a+3, [](const Student& val1, const Student& val2){ return val1.ID < val2.ID; }); for_each(a, a+3, [](const Student& val){cout<<val.ID<<’ ‘<<val.name<<endl;}); return 0 } 其中的for_each句用於輸出――其中的Lambda運算式意味著:對於每一個val,輸出其ID與Name值――這樣我們連for迴圈也省了。 Lambda運算式的引入就是為了更方便地書寫程式,更容易地閱讀程式。如同STL一樣,有什麼理由不去用呢? Lambda運算式的基本文法 有了感性的認識後,我們來分析一下Lambda運算式的文法。 我這裡無意把C++標準草案中Lambda運算式的有關章節翻譯過來(我也不佩這麼做)。只是在這裡希望以最通俗的方式將它的文法講解一二。從結構上說,Lambda運算式可以寫成如下的形式: Lambda-introducer lambda-declarator(opt) compound-statement 其中的Lambda-introducer就是剛剛的那個“[]”它是不能省略的。中括弧中也可能出現變數。表示將局部變數傳入到Lambda運算式中。lambda-declaratoropt是可選擇的,包括了運算式的參數列表,傳回值資訊,mutable聲明(以及一些其它資訊,這裡不做討論)。而最後的compound-statement則是運算式的主要內容。 還是看一個例子吧: int n = 10; [n](int k) mutable -> int { return k + n; }; 程式的第二行是一個lambda運算式,lambda裡能出現的東西幾乎全了(當然,正如我在前文說的,有一些其它資訊這裡不做討論,所以沒有加入其中)。讓我們對裡面的東西一一分析: l[n]是Lambda-introducer,而n是一個變數,表明該運算式範圍中的變數n將被傳入這個運算式。以本程式為例,傳入的值是10。Lambda-introducer可以指定變數以值的方式傳入,也可以用其它的形式指定其以引用的方式傳入。其變型大家就baidu一下吧J l(int k)表示了參數列表,屬於lambda-declarator的一部分。你可以把運算式看成一個仿函數(如上文的)。這裡指定了仿函數的參數列表。如果函數的參數列表為空白,這一部分可以省略。 lmutable表示仿函數中的變數能否改變。以前文中compareID這個仿函數為例,注意到其中的operator ()是const的。如果lambda運算式中引入了這個mutable,則對應的仿函數中operator()的定義將不包含這個const――這意味著仿函數中的變數值(Lambda-introducer傳入)可以改變。討論operator() const與operator()的區別已經超出了本文的範圍,想瞭解的話,看看C++相關教程吧J l-> int表示傳回型別(這裡是int)。如果編譯器能從代碼中推斷出傳回型別,或者Lambda運算式的傳回型別為void,則該項可省略; l{ return k+n; }是compound-statement:函數體。 通過分析可以看出,這個Lambda運算式相當於一個函數,該函數讀入一個int值k,將該值加上n返回。根據上述說明,這個運算式可以簡寫為: [n](int k){ return k + n; }; Lambda運算式可以儲存在std::function<T>或std:: reference_closure<T>類型的變數中。其中的T表示了運算式對應函數的類型。以上述運算式為例,它輸入參數為int型變數,輸出為int,那麼為了儲存它,可以寫成如下的形式: function<int(int)> g = [n](int k){ return k + n; }; 另一個例子,前文所使用的Lambda運算式: [](const Student& val1, const Student& val2){ return val1.ID < val2.ID; } 可以儲存於function<bool(constStudent&,constStudent&)>這個類型的變數中。 如果你嫌這麼寫麻煩,也可以利用C++新標準中另一個新特性:類型推導。即用auto作為變數的類型,讓編譯器自己推導運算式的類型: auto g = [n](int k){ return k + n; }; 沒問題,這樣寫g還是一個強型別的變數,只不過其類型是由編譯器推導的,好處是你不用寫太長的變數類型了J Lambda運算式進階 作為結尾,我們來看一些C++ Lambda運算式進階的用法。 Lambda運算式被引入主要是用於函數式編程。有了Lambda運算式,我們也可以做一些函數式編程的東西。比如將一個函數作為傳回值的應用: auto g = [](int n) -> function<void (int)> { return [n](int k){ cout<<n+k<<’ ‘; }; }; 它是一個Lambda運算式,輸入一個整型變數n,返回一個函數(lambda運算式),這個函數接收一個int值k,並列印出k+n。g的使用方法如下: int a[]={1,2,3,4,5,6,7,8,9,0}; function<void (int)> f = g(2); for_each(a, a+10, f); 它將輸出:3 4 5 6 7 8 9 10 11 2 有一點函數式編程的味道了J 至於其它的東西,比如如下的運算式: [](){}(); 是一個有效調用。其中“[](){}”表示一個Lambda運算式,其輸入參數為空白,返回void,什麼都不幹。而最後的()表示調用其求值――雖然什麼都不幹,但編譯能通過,很唬人喔J 好了,就寫到這裡吧。關於Lambda運算式想說的最後一件事是:它是新標準C++11中定義的。老的編譯器不支援(這也是我用VS2010的原因)。想要用它,以及其它新標準帶來的好處嗎?嘿,你的傢伙(指編譯器)該升級了J |