lambda表示式與std::function
在這篇文章中,我們將探討 lambda 在不同方面的表現。然後我們將研究 std::function 及其工作原理。
什麼是lambda
如果你還沒用過C++11最強大的特徵之一——lambda,我就來做一個簡短的介紹:
Lambda是匿名函式的別稱。從本質上講,它們是一種在程式碼的邏輯位置編寫函式(比如回撥函式)的簡單方法。
我最喜歡的C++表示式是 [](){}();
,它聲明瞭一個空的lambda並且立即執行它。這個表示式顯然沒有任何功能作用,只是告訴你lambda表示式的格式。更好的一個例子是跟STL結合:
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
與C++98方法相比,它具有以下優點:它是程式碼在邏輯上的位置(而不是在此範圍之外定義類/函式),並且不會汙染任何名稱空間(儘管即使在C++98中也很容易繞過)。
lambda語法
Lambdas 分為3部分:
[capture]
: 捕獲列表 - 捕捉列表總是出現在Lambda函式的開始處。實際上,[]是Lambda引出符。編譯器根據該引出符判斷接下來的程式碼是否是Lambda函式。捕捉列表能夠捕捉上下文中的變數以供Lambda函式使用;(parameters)
: 引數列表 – 與普通函式的引數列表一致。如果不需要引數傳遞,則可以連同括號“()”一起省略;{statement}
: 函式體 – 內容與普通函式一樣,不過除了可以使用引數之外,還可以使用所有捕獲的變數。
舉個例子:
int i = 0, j = 1;
auto func = [i, &j](bool b, float f){ ++j; cout << i << ", " << b << ", " << f << endl; };
func(true, 1.0f);
- 第一行很簡單 - 建立兩個
int
變數 命名為i
和j
. - 第二行定義了一個lambda表示式:
- 通過值傳遞捕捉變數
i
,通過引用傳遞捕捉變數j
- 接受2個引數:
bool b
和float f
, - 呼叫時列印
b
和f
- 通過值傳遞捕捉變數
- 第三行用
true
和1.0f
我們可以把lambda表示式看做類:
- 捕捉列表是資料成員:
func
的資料成員是i
和j
;- lambda可以在其程式碼範圍內訪問這些成員.
- 建立lambda時,建構函式將捕獲的變數複製到資料成員;
- 這個類有
operator()(...)
(對於func
來說...
就是bool, float
); - 它有一個作用域生存期和一個釋放成員的解構函式.
語法方面的最後一點:你還可以指定預設捕獲:
-
[var]表示值傳遞方式捕捉變數var;
-
[=]表示值傳遞方式捕捉所有父作用域的變數(包括this);
-
[&var]表示引用傳遞捕捉變數var;
-
[&]表示引用傳遞方式捕捉所有父作用域的變數(包括this);
-
[this]表示值傳遞方式捕捉當前的this指標。
上面提到了一個父作用域,也就是包含Lambda函式的語句塊,說通俗點就是包含Lambda的“{}”程式碼塊。上面的捕捉列表還可以進行組合,例如:
- [=,&a,&b]表示以引用傳遞的方式捕捉變數a和b,以值傳遞方式捕捉其它所有變數;
- [&,a,this]表示以值傳遞的方式捕捉變數a和this,引用傳遞方式捕捉其它所有變數。
不過值得注意的是,捕捉列表不允許變數重複傳遞。下面一些例子就是典型的重複,會導致編譯時期的錯誤。例如:
- [=,a]這裡已經以值傳遞方式捕捉了所有變數,但是重複捕捉a了,會報錯的;
- [&,&this]這裡&已經以引用傳遞方式捕捉了所有變數,再捕捉this也是一種重複。
值傳遞VS引用傳遞
上面我們提到了通過值和通過引用捕獲lambda。有什麼區別?下面是一個簡單的程式碼,可以說明:
int i = 0;
auto foo = [i](){ cout << i << endl; };
auto bar = [&i](){ cout << i << endl; };
i = 10;
foo();
bar();
輸出結果:
0
10
可以看出,值傳遞傳入的是值,如果傳入的是一個變數,相當於傳遞了一個副本,不會改變原有變數。引用傳遞傳遞的是一個指標(c++裡也有引用),會改變原變數的值。
lambda作用域
所有捕捉到的變數作用域都在lambda範圍內:
#include <iostream>
#include <functional>
struct MyStruct {
MyStruct() { std::cout << "Constructed" << std::endl; }
MyStruct(MyStruct const&) { std::cout << "Copy-Constructed" << std::endl; }
~MyStruct() { std::cout << "Destructed" << std::endl; }
};
int main() {
std::cout << "Creating MyStruct..." << std::endl;
MyStruct ms;
{
std::cout << "Creating lambda..." << std::endl;
auto f = [ms](){}; // note 'ms' is captured by-value
std::cout << "Destroying lambda..." << std::endl;
}
std::cout << "Destroying MyStruct..." << std::endl;
}
輸出:
Creating MyStruct...
Constructed
Creating lambda...
Copy-Constructed
Destroying lambda...
Destructed
Destroying MyStruct...
Destructed
mutable
lambda
lambda的 operator()
預設是const, 這意味著它不能直接修改捕捉到的變數. 要想修改的話需要新增 mutable
:
int i = 1;
[&i](){ i = 1; }; // ok, 'i' 是引用傳遞捕捉到的.
[i](){ i = 1; }; // ERROR: 'i'是隻讀變數.
[i]() mutable { i = 1; }; // ok.
lambda可以直接複製,就像類一樣:
int i = 0;
auto x = [i]() mutable { cout << ++i << endl; }
x();
auto y = x;
x();
y();
輸出:
1
2
2
lambda表示式的大小
因為lambda有捕獲,所以lambda沒有固定大小。舉個例子:
auto f1 = [](){};
cout << sizeof(f1) << endl;
std::array<char, 100> ar;
auto f2 = [&ar](){};
cout << sizeof(f2) << endl;
auto f3 = [ar](){};
cout << sizeof(f3) << endl;
輸出 (64位下):
1
8
100
效能
Lambda在效能方面也非常出色。因為它們是物件而不是指標,所以編譯器可以很容易地內聯它們,就像仿函式一樣。這意味著多次呼叫lambda(例如使用std::sort
或std::copy_if
)比使用全域性函式要好得多。這是C++的實際速度比C快的一個例子。
std::function
std::function
是一個模板化物件,用於儲存和呼叫任何可呼叫型別,例如函式、物件、lambda 和 std::bind
的結果。
舉例
#include <iostream>
#include <functional>
using namespace std;
void global_f() {
cout << "global_f()" << endl;
}
struct Functor {
void operator()() { cout << "Functor" << endl; }
};
int main() {
std::function<void()> f;
cout << "sizeof(f) == " << sizeof(f) << endl;
f = global_f;
f();
f = [](){ cout << "Lambda" << endl;};
f();
Functor functor;
f = functor;
f();
}
輸出:
$ clang++ main.cpp -std=c++14 && ./a.out
sizeof(f) == 32
global_f()
Lambda
Functor