lambda 運算式


C++ 11 可以使用 lambda 運算式,可以在函式中封裝一段演算流程進行傳遞,例如,在〈函式指標〉的範例中,定義了 ascendingdescending 函式以便傳遞,如果事先這兩個函式並不存在,你想在 main 直接傳遞比序演算,C++ 11 以後可以如下:

#include <iostream> 
#include <functional>
#include <algorithm>
using namespace std; 

int main() { 
    int number[] = {3, 5, 1, 6, 9};
    auto print = [](int n) { cout << n << " "; };

    sort(begin(number), end(number), [](int n1, int n2) { return n2 - n1; });
    // 顯示 9 6 1 5 3
    for_each(begin(number), end(number), print);
    cout << endl;

    sort(begin(number), end(number), [](int n1, int n2) { return n1 - n2; });
    // 顯示 3 5 1 6 9
    for_each(begin(number), end(number), print);
    cout << endl;

    return 0; 
} 

在上頭你看到了幾個 [] 開頭的運算式,這些運算式是 lambda 運算式,你也看到了 sortfor_each,這些是定義在 algorithm 的函式,可以給它陣列開頭與結尾的位址,並傳遞一段演算,聲明想對陣列做些什麼,sort 是指定了比序的依據,而 for_each 指定了 print 定義的演算,也就是接受陣列元素值並顯示在標準輸出。

lambda 運算式定義了一個 Callable 物件,也就是個可以接受呼叫操作的物件,例如函式就是其中之一。來看看 lambda 運算式的定義方式:

[ captures ] ( params ) -> ret { body }
[ captures ] ( params ) { body }
[ captures ] { body }

簡單來說,( params ) -> ret 可以依需求撰寫,來看看方才範例中的 print

auto print = [](int n) { cout << n << " "; };

這定義了一個 Callable 物件,呼叫時可以接受一個引數,因為沒有 return,也沒有定義 lambda 運算式的傳回型態,就自動推斷為 ret 的部份為 void,也就是相當於:

auto print = [](int n) -> void { cout << n << " "; };

那麼 print 的型態是什麼呢?lambda 運算式會建立一個匿名類別(稱為 closure type)的實例,因為無法取得匿名類別的名稱,也就無法宣告其型態,因而大多使用 auto 來自動推斷。

然而這就有一個問題,若要定義一個函式可以接受 lambda 運算式,參數無法使用 auto,怎麼辦呢?可以包含 functional 標頭檔,使用 function 來宣告,function 的實例可以接受 Callable 物件,lambda 運算式是其中之一,例如:

function<void(int)> print = [](int n) { cout << n << " "; };

若 lambda 運算式被指定給函式指標,那麼 lambda 運算式建立的實例會轉換為位址:

void (*f)(int) = [](int n) { cout << n << " "; };

因此,既有的函式若參數是函式指標型態,也可以接受 lambda 運算式。

lambda 運算式的本體若有 return,然而沒有定義 ret 的型態時,會自動推斷,因此底下 f1ret 型態會自動推斷為 int

auto f1 = [](int n1, int n2) { return n2 - n1; }
auto f2 = [](int n1, int n2) -> int { return n2 - n1; };

接下來看 [capture],在若只定義為 [] 時,沒辦法使用任何 lambda 運算式外部的變數,若想運用外部變數,定義時基本上從 =& 出發:

  • [=]:lambda 運算式本體可以取用外部變數。
  • [&]:lambda 運算式本體可以參考外部變數。

使用 = 時,lambda 運算式本體中取用到某外部變數時,其實是隱含地建立了同名、同型態的區域變數,然後將外部變數的值複製給區域變數,預設情況下不能修改,然而可以加上 mutable 修飾,不過要注意的是,這時修改的會是區域變數的值,不是外部變數。例如:

#include <iostream> 
using namespace std; 

int main() { 
    int x = 10;

    auto f = [=]() mutable -> void {
        x = 20;
        cout << x << endl;
    };

    f(); // 顯示 20
    cout << x << endl; // 顯示 10

    return 0; 
} 

使用 = 時,lambda 運算式本體中參考外部變數時,其實是隱含地建立了同名的參考,因此在 lambda 運算式本體中修改變數,另一變數取值就也會是修改過的結果:

#include <iostream> 
using namespace std; 

int main() { 
    int x = 10;

    auto f = [&]() mutable -> void {
        x = 20;
        cout << x << endl;
    };

    f(); // 顯示 20
    cout << x << endl; // 顯示 20

    return 0; 
} 

[capture] 可以限定捕捉的變數有哪些,以及以哪種方式捕捉:

  • [x, y]:以 = 的方式取用外部的 xy
  • [x, &y]:以 = 取用外部的 x,以 & 的方式參考外部的 y
  • [=, &y]:以 & 的方式參考外部的 y,其餘外部變數取用時都是 = 的方式。
  • [&, y]:以 = 的方式參考外部的 y,其餘外部變數以 & 的方式參考。

要設置預設捕捉方式時,對於沒指定捕捉方式的其他變數,就會採用預設捕捉方式。

若有必要,lambda 運算式建立之後也可以馬上呼叫,例如:

#include <iostream> 
using namespace std; 

int main() { 
    // 顯示 Hello, Justin
    [](const char *name) {
        cout << "Hello, " << name << endl;
    }("Justin");
    return 0; 
} 

在定義模版(template)時,lambda 運算式也可以模版化。例如:

template <typename T>
function<T(T)> negate_all(T t1) {
    return [=](T t2) -> T {
        return t1 + t2;
    };
}

在 C++ 14,捕捉變數時,可以建立新變數並指定其值,新變數的型態會自動推斷。例如:

auto print = [x = 10](int n) { cout << n + x << " "; };

雖然函式的參數型態不能以 auto 宣告,然而在 C++ 14,lambda 運算式的參數型態可以是 auto

#include <iostream> 
using namespace std; 

int main() { 
    auto plus = [] (auto a, auto b) {
        return a + b;
    };

    // 顯示 3
    cout << plus(1, 2) << endl; 

    // 顯示 abcxyz
    cout << plus(string("abc"), string("xyz")) << endl;

    return 0; 
} 

指定給 plus 的 lambda 運算式,稱為泛型 lambda(generic lambda),原理是基於模版,引數型態只要符合本體中的實作協定就可以用來呼叫 lambda 運算式,在上例中就是引數要能使用 + 運算子處理。