C++0x:Lambda expression


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,如果在外部變數只有 xy 的情況下,這三種寫法會是等價的。

  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 裡沒有加上 xy 的話,是會因為在 lambda function 裡找不到變數 xy 而編譯錯誤的。

如果是要用 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
class std::tr1::function
class `anonymous namespace'::
class `anonymous namespace'::

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

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

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

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

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

參考資料:

廣告

關於 Heresy
https://kheresy.wordpress.com

17 Responses to C++0x:Lambda expression

  1. 引用通告: C++14 到 C++17 的變化 | Heresy's Space

  2. 引用通告: C++ 幾種函式傳遞方法的效能比較 | Heresy's Space

  3. 引用通告: Boost Log 的 attribute 的簡易使用 | Heresy's Space

  4. 引用通告: 程式的記錄輔助工具:Boost Log | Heresy's Space

  5. 小關 says:

    3. mutable
    原因應該是reference抓進來的有可能是const reference,所以才需要mutable吧!(我猜)

    Liked by 1 person

    • Heresy says:

      這個就不確定了。等哪天研究出來再來分享吧~(茶

      按讚數

  6. 引用通告: C++14 進行中,來看目前的委員會草案吧~ | Heresy's Space

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

您的留言將使用 WordPress.com 帳號。 登出 / 變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 / 變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 / 變更 )

Google+ photo

您的留言將使用 Google+ 帳號。 登出 / 變更 )

連結到 %s

%d 位部落客按了讚: