C++ 11 可以使用 lambda 運算式,可以在函式中封裝一段演算流程進行傳遞,例如,在〈函式指標〉的範例中,定義了 ascending
、descending
函式以便傳遞,如果事先這兩個函式並不存在,你想在 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 運算式,你也看到了 sort
、for_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
的型態時,會自動推斷,因此底下 f1
的 ret
型態會自動推斷為 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]
:以=
的方式取用外部的x
、y
。[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 運算式,在上例中就是引數要能使用 +
運算子處理。