1. 程式人生 > 其它 >lambda表示式與std::function

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部分:

  1. [capture] : 捕獲列表 - 捕捉列表總是出現在Lambda函式的開始處。實際上,[]是Lambda引出符。編譯器根據該引出符判斷接下來的程式碼是否是Lambda函式。捕捉列表能夠捕捉上下文中的變數以供Lambda函式使用;
  2. (parameters) : 引數列表 – 與普通函式的引數列表一致。如果不需要引數傳遞,則可以連同括號“()”一起省略;
  3. {statement} : 函式體 – 內容與普通函式一樣,不過除了可以使用引數之外,還可以使用所有捕獲的變數。

舉個例子:

int i = 0, j = 1;
auto func = [i, &j](bool b, float f){ ++j; cout << i << ", " << b << ", " << f << endl; };
func(true, 1.0f);
  1. 第一行很簡單 - 建立兩個 int變數 命名為 ij.
  2. 第二行定義了一個lambda表示式:
    • 通過值傳遞捕捉變數 i,通過引用傳遞捕捉變數 j
    • 接受2個引數: bool bfloat f,
    • 呼叫時列印 bf
  3. 第三行用true1.0f
    為引數呼叫lambda.

我們可以把lambda表示式看做類:

  • 捕捉列表是資料成員:
    • func 的資料成員是 ij;
    • 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::sortstd::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