1. 程式人生 > >C++中的lambda表示式

C++中的lambda表示式


title: 知識點梳理:C++中的lambda表示式
categories: Cpp
date: 2018-09-20 22:08:39
tags:

  • 知識點梳理

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,則可以改變,如下所示:

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)。