C++ 中的lambda表達式

C++中的lambda與函數對象

lambda表達式是C++11中引入的一項新技術,利用lambda表達式可以編寫內嵌的匿名函數,用以替換獨立函數或者函數對象,並且使程式碼更可讀。但是從本質上來講,lambda表達式只是一種語法糖,因為所有其能完成的工作都可以用其它稍微複雜的程式碼來實現。但是它簡便的語法卻給C++帶來了深遠的影響。

如果從廣義上說,lambda表達式產生的是函數對象。函數對象的本質上是一個類而不是一個函數,在類中,對象重載了函數調用運算符(),從而使對象能夠項函數一樣被調用,我們稱這些對象為函數對象(Function Object)或者仿函數(Functor)。相比lambda表達式,函數對象有自己獨特的優勢。下面我們開始具體講解這兩項黑科技。

lambda表達式

先從一個簡單的例子開始,我們定義一個輸出字元串的lambda表達式,如下所示,表達式一般都是從方括弧[]開始,然後結束於花括弧{}

auto basic_lambda = [] {cout << "Hello Lambda" << endl;}; //定義簡單的lambda表達式
basic_lambda(); //調用

下面分別是包含參數和返回類型的lambda表達式:

auto add = [] (int a, int b)->int { return a + b;}; //返回類型需要用`->`符號指出
auto multiply = [](int a, int b) {return a * b;} //一般可以省略返回類型,通過自動推斷就能得到返回類型

lambda表達式最前面的方括弧提供了「閉包」功能。每當定義一個lambda表達式以後,編譯器會自動生成一個 匿名類 ,並且這個類重載了()運算符,我們將其稱之為閉包類型(closure type)。在運行時,這個lambda表達式會返回一個匿名的閉包實例,並且該實例是一個右值。閉包的一個強大之處在於其可以通過傳值或引用的方式捕捉其封裝作用域內的變數,lambda表達式前面的方括弧就是用來定義捕捉模式以及變數的lambda捕捉塊,如下所示:

int main() {
    int x = 10; // 定義作用域內的x,方便下面的lambda捕捉
    auto add_x = [x](int a) { return a + x;}; // 傳值捕捉x
    auto multiply_x = [&x](int a) {return a * x;}; //引用捕捉x
}

lambda捕捉塊為空時,表示沒有捕捉任何變數。對於傳值方式捕捉的變數x,lambda表達式會在生成的匿名類中添加一個非靜態的數據成員,由於閉包類重載()運算符是使用了const屬性,所以不能在lambda表達式中修改傳值方式捕捉的變數,但是如果把lambda標記為mutable,則可以改變(但是這裡的改變只會對 lambda 表達式內部的程式碼有影響, 對外部不起作用),如下所示:

int x = 10;

auto add_x = [x](int a) mutable { x * = 2; return a + x;};
cout << add_x(10) << endk; //輸出30
return 0;

而對於引用方式捕捉的變數,無論是否標記為mutable,都可以對變數進行修改,並且修改的值會影響到外部, 至於會不會在匿名類中創建數據成員,需要看不同編譯器的具體實現。

lambda表達式只能作為右值,也就是說,它是不能被賦值的

auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };

a = b;  // 非法,lambda表達式變數只能做右值
auto c = a; // 合法,生成一個副本

造成以上原因是因為禁用了賦值運算符:

ClosureType &operator=(const ClosureType &) = delete;

但是沒有禁用複製構造函數,所以仍然可以用是一個lambda表達式去初始化另一個(通過產生副本)。

關於lambda的捕捉塊,主要有以下用法:

  • []:默認不捕捉變數
  • [=]:默認以值捕捉所有變數(最好不要用)
  • [&]:默認以引用捕捉所有變數(最好不要用)
  • [x]:僅以值捕捉變數x,其他變數不捕捉
  • [&x]:僅以引用捕捉x,其他變數不捕捉
  • [=, &x]:默認以值捕捉所有變數,但是x是例外,通過引用捕捉
  • [&, x]:默認以引用捕捉所有變數,但是x是例外,通過值捕捉
  • [this]:通過引用捕捉當前對象(其實是複製指針)
  • [* this]:通過傳值方式捕捉當前對象

通過以上的說明,可以看到lambda表達式可以作為返回值,賦值給對應類型的函數指針,但是使用函數指針貌似並不是那麼方便,於是STL在頭文件<functional>中定義了一個多態的函數對象封裝std::function,其功能類似於函數指針。它可以綁定到任何類函數對象,只要參數與返回類型相同。如下面的返回一個bool且接收兩個int的函數包裝器:

std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };

lambda表達式還有一個很重要的應用是其可以作為函數的參數,如下所示:

int value = 3;
vector<int> v{1, 2, 3, 4, 5, 6, 7};

int count == std::count_if(v.begin, v.end(), [value](int x) {return x > value;});

下面給出lambda表達式的完整語法:

// 完整語法
[ capture - list ] ( params ) mutable(optional) constexpr(optional)(c++17) exception attribute -> ret { body }

// 可選的簡化語法
[ capture - list ] ( params ) -> ret { body }
[ capture - list ] ( params ) { body }
[ capture - list ] { body }
  • capture-list:捕捉列表,這個不用多說,前面已經講過,記住它不能省略;
  • params:參數列表,可以省略(但是後面必須緊跟函數體);
  • mutable:可選,將lambda表達式標記為mutable後,函數體就可以修改傳值方式捕獲的變數;
  • constexpr:可選,C++17,可以指定lambda表達式是一個常量函數;
  • exception:可選,指定lambda表達式可以拋出的異常;
  • attribute:可選,指定lambda表達式的特性;
  • ret:可選,返回值類型;
  • body:函數執行體。

lambda新特性(C++14)

C++14中,lambda又得到了增強,一個是泛型lambda表達式,一個是lambda可以捕捉表達式。

lambda捕捉表達式

前面講過,lambda表達式可以按傳值或者引用捕捉在其作用域範圍內的變數。而有時候,我們希望捕捉不在其作用域範圍內的變數,而且最重要的是我們希望捕捉右值。所以C++14中引入了表達式捕捉,其允許用任何類型的表達式初始化捕捉的變數,如下:

// 利用表達式捕獲,可以更靈活地處理作用域內的變數
int x = 4;
auto y = [&r = x, x = x + 1] { r += 2; return x * x; }();
// 此時 x 更新為6,y 為25

// 直接用字面值初始化變數
auto z = [str = "string"] { return str; }();
// 此時z是const char* 類型,存儲字元串 string

可以看到捕捉表達式擴大了lambda表達式的捕捉能力,有時候你可以用std::move初始化變數。這對不能複製只能移動的對象很重要,比如 std::unique_ptr,因為其不支援複製操作,你無法以值方式捕捉到它。但是利用lambda捕捉表達式,可以通過移動來捕捉它:

auto myPi = std::make_unique<double>(3.1415);
auto circle_area = [pi = std::move(myPi)](double r) { return *pi * r * r; };
cout << circle_area(1.0) << endl; // 3.1415

泛型lambda表達式

從C++14開始,lambda表達式支援泛型:其參數可以使用自動推斷類型的功能,而不需要顯示地聲明具體類型。這就如同函數模板一樣,參數要使用類型自動推斷功能,只需要將其類型指定為auto,類型推斷規則與函數模板一樣。這裡給出一個簡單例子:

auto add = [](auto x, auto y) { return x + y; };

int x = add(2, 3);   // 5
double y = add(2.5, 3.5);  // 6.0

函數對象

函數對象是一個廣泛的概念,因為所有具有函數行為的對象都可以稱為函數對象。這是一個高級抽象,我們不關心對象到底是什麼,只要其具有函數行為即可。函數行為是指可以使用()調用並傳遞參數,如下所示:

function(arg1, arg2, ...); //函數調用

由此,lambda表達式也是一個函數對象。該函數對象實際上是一個匿名類的實例,且這個類實現了函數調用運算符()

泛型提供了高級抽象,不論是lambda表達式、函數對象、還是函數指針,都可以傳入到STL演算法中(如for_each)。