C++0x:Lambda expression

| | 0 Comments| 09:25
Categories:

Lambda expression 是之前《C 語法再加強:C 0x》一文中列出的 MSVC 10 在 C core language 加入的第三項新功能。他的基本概念是一個「匿名函式 (anonymous function)」,可以用來快速地建立一個沒有名稱的 function object 來使用。也因此,除了宣告的方法以及使用目的不太一樣外,實際上他很類似一般的 function。

而 lambda expression 的語法定義(參考 MSDN),則如下:

[]() mutable throw() -> typeid
{
  //function body
}

要細分的話,可以把它拆成六個部份,個別的意義分別是:

  1. [] : lambda introducer, capture clause

    lambda expression 基本上就是由 [] 開始的,而且也是 lambda expression 的語法中絕對不能省略的部分;它的目的是用來告訴編譯器接下來的就是要開始寫 lambda expression 了∼

    不過實際上除了 introducer 的功能外,他還包含了所謂「capture」的功能,可以把 lambda expression 所在的 scope 內可以讀到的變數抓到 lambda expression 裡使用;而使用上也可以設定為 by value 或 by reference。除了一個一個變數設定外,可以直接使用 =& 設定預設 capture(capture-default);前者是預設將所有變數以 by value 的方式抓進來、後者則是將所有變數以 by reference 的方式抓進來。

    下面是用比較單純的例子來說明:

      1: []      // 不使用外部的變數
      2: [=]     // 全部 capture by value
      3: [&]     // 全部 capture by reference
      4: [x, &y] // x by value, y by reference
      5: [=, &y] // 除了 y by reference 外,其他全部 by value
      6: [&, x]  // 除了 x by value 外,其他全部 by reference

    其中要注意的是,capture-default(=、&)要放在 capture list 的第一項。

    此外,上面的例子裡的 4/5/6,如果在外部變數只有 x 和 y 的情況下,這三種寫法會是等價的。

  2. () : parameter declaration list

    要傳入這個匿名函釋的變數,基本上就像一般的 function 一樣的用法,不過多加了一些限制:

    • 不能有預設值
    • 不能有可變長度的參數列表
    • 不能有沒命名的參數

    另外,在不需要傳參數進 lambda expression 的時候,是可以直接把 () 省略的。

  3. mutable : mutable specification

    可以省略的東西。加上了 mutable 後,是讓 lambda expression 可以修改 capture by value 的外部變數。
    (Heresy 不太瞭的是,要修改的用 by reference 的方法抓近來不就好了?)

  4. throw() : exception specification

    可以省略的東西。這其實是一般函式就可以加的功能,可以用來指定這個函式會不會丟出例外狀況(exception)、丟出哪種類型的例外狀況;詳細可以參考《C Exception Handling》和《Exception Specifications》。

  5. -> typeid : return type clause

    指定 lambda 回傳值的型別。在 lambda body 沒有回傳(return)或是只有一種回傳路徑的情況下,編譯器會自動判斷回傳的型別,所以可以把這部分也省略掉。

  6. {…} : function body

    這個 lambda expression 要做的事,就像一般 function 的程式內容。

而 lambda expression 的好處,就是可以不用實際宣告出函式,而直接拿來當 funciton object 使用。實際使用的時候,最普遍的用法應該是用在像是 STL <algorithm> 裡的函式,把 function 當參數傳進去了∼

例如下面的程式就是在 C 98 時,要使用 for_each() 時的寫法:

class LambdaFunctor
{
public:
  void operator()(int n) const {
    cout << n << " ";
  }
};
 
int main() {
  vector<int> v;
  for( int i = 0; i < 10;   i )
    v.push_back(i);
  for_each( v.begin(), v.end(), LambdaFunctor() );
  cout << endl;
}

而如果改用 lambda expression 的話,就可以變成:

int main() {
  vector<int> v;
  for (int i = 0; i < 10;   i)
    v.push_back(i);
  for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
  cout << endl;
}

這樣應該很明顯看得出來,使用 lambda expression 的好處,就是可以不必額外寫出 LambdaFunctor 這個 function class 了∼

而如果要使用 capture-list 的話,下方是一個簡單的例子,他會把 vector v 中值介於 4 到 7 之間的項目給刪除掉:

int main()
{
  vector<int> v;
  for( int i = 0; i < 10;   i )
    v.push_back(i);
 
  int x = 4;
  int y = 7;
 
  v.erase( remove_if( v.begin(), v.end(), 
                      [x, y](int n) { return x < n && n < y; } ),
           v.end());
  for_each(v.begin(), v.end(), [](int n) { cout << n << " "; });
  cout << endl;
}

如果在 lambda introducer 裡的 capture list 裡沒有加上 x, y 的話,是會因為在 lambda function 裡找不到變數 x, y 而編譯錯誤的。

如果是要用 by reference 的話,下面是一個把 vector 加總的簡單範例:

vector<int> v;
...
 
int sum = 0;
for_each( v.begin(), v.end(), [&sum]( int n ){ sum  = n; } );

最後,由於 lambda expression 會產生類似 function obejct 的物件,所以其實我們可以用 auto 或是 tr1 的 function 來把 lambda expression 儲存下來;不過可能要注意的是,如果是用 auto 來儲存 lambda expression 的話,每次都會是不同的型別的!下面就是一個簡單的範例:

#include <functional>
#include <iostream>
 
using namespace std;
 
int main()
{
  function<void (int)> g1 = [](int n){cout << n;};
  function<void (int)> g2 = [](int n){cout << n;};
  auto g3 = [](int n){cout << n;};
  auto g4 = [](int n){cout << n;};
  
  cout << typeid( g1 ).name() << endl;
  cout << typeid( g2 ).name() << endl;
  cout << typeid( g3 ).name() << endl;
  cout << typeid( g4 ).name() << endl;
}

這樣的一段程式用 VC10 編譯後執行的結果會是:

class std::tr1::function<void __cdecl(int)>
class std::tr1::function<void __cdecl(int)>
class `anonymous namespace'::<lambda2>
class `anonymous namespace'::<lambda3>

其中可以發現,如果用 auto 來儲存 lambda expression 的話,兩個同樣寫法的 lambda expression 會被當成兩種不同型別的物件,所以兩者也是不能互換的∼

另外一提,由於 lambda expression 本身可以直接當作 function object 來使用,所以下面的語法是合法的,就相當於直接呼叫這個沒有名稱的 function object:

int a = [](){return 5;}();

甚至,下面這兩行看起來很詭異的程式,也是合法的語法∼

[](){}();
[]{}();

參考資料:

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *