1. 程式人生 > 程式設計 >一文讀懂c++11 Lambda表示式

一文讀懂c++11 Lambda表示式

1.簡介

1.1定義

C++11新增了很多特性,Lambda表示式(Lambda expression)就是其中之一,很多語言都提供了 Lambda 表示式,如 Python,Java ,C#等。本質上, Lambda 表示式是一個可呼叫的程式碼單元[1]^{[1]}[1]。實際上是一個閉包(closure),類似於一個匿名函式,擁有捕獲所在作用域中變數的能力,能夠將函式做為物件一樣使用,通常用來實現回撥函式、代理等功能。Lambda表示式是函數語言程式設計的基礎,C++11引入了Lambda則彌補了C++在函數語言程式設計方面的空缺。

1.2作用

以往C++需要傳入一個函式的時候,必須事先進行宣告,視情況可以宣告為一個普通函式然後傳入函式指標,或者宣告一個仿函式(functor,函式物件),然後傳入一個物件。比如C++的STL中很多演算法函式模板需要傳入謂詞(predicate)來作為判斷條件,如排序演算法sort。謂詞就是一個可呼叫的表示式,其返回結果是一個能用作條件的值。標準庫演算法所使用的謂詞分為兩類:一元謂詞(unary predicate,只接受單一引數)和二元謂詞(binary predicate,接受兩個引數)。接受謂詞的演算法對輸入序列中的元素呼叫謂詞,因此元素型別必須能轉換為謂詞的引數型別。如下面使用sort()傳入比較函式shorter()(這裡的比較函式shorter()就是謂詞)將字串按長度由短至長排列。

//謂詞:比較函式,用來按長度排列字串
bool shorter(const string& s1,const string& s2)
{
 return s1.size()<s2.size();
}

//按長度由短至長排列words
std::sort(words.begin(),words.end(),shorter);

Lambda表示式可以像函式指標、仿函式一樣,作為一個可呼叫物件(callable object)被使用,比如作為謂詞傳入標準庫演算法。

也許有人會問,有了函式指標、函式物件為何還要引入Lambda呢?函式物件能維護狀態,但語法開銷大,而函式指標語法開銷小,卻沒法儲存函式體內的狀態。如果你覺得魚和熊掌不可兼得,那你可錯了。Lambda函式結合了兩者的優點,讓你寫出優雅簡潔的程式碼。

1.3語法格式

Lambda 表示式就是一個可呼叫的程式碼單元,我們可以將其理解為一個未命名的行內函數。與任何函式類似,一個Lambda具有一個返回型別、一個引數列表和一個函式體。但與函式不同,Lambda可以定義在函式內部,其語法格式如下:

[capture list](parameter list) mutable(可選) 異常屬性->return type{function body}

capture list(捕獲列表)是一個Lambda所在函式中定義的區域性變數的列表,通常為空,表示Lambda不使用它所在函式中的任何區域性變數。parameter list(引數列表)、return type(返回型別)、function body(函式體)與任何普通函式基本一致,但是Lambda的引數列表不能有預設引數,且必須使用尾置返回型別。 mutable表示Lambda能夠修改捕獲的變數,省略了mutable,則不能修改。異常屬性則指定Lambda可能會丟擲的異常型別。

其中Lambda表示式必須的部分只有capture list和function body。在Lambda忽略引數列表時表示指定一個空引數列表,忽略返回型別時,Lambda可根據函式體中的程式碼推斷出返回型別。例如:

auto f=[]{return 42;}

我們定義了一個可呼叫物件f,它不接受任何引數,返回42。auto關鍵字實際會將 Lambda 表示式轉換成一種類似於std::function的內部型別(但並不是std::function型別,雖然與std::function“相容”)。所以,我們也可以這麼寫:

std::function<int()> Lambda = [] () -> int { return val * 100;};

如果你對std::function<int()>這種寫法感到很神奇,可以檢視 C++ 11 的有關std::function的用法。簡單來說,std::function<int()>是一個例項化後的模板類,代表一個可呼叫的物件,接受 0 個引數,返回值是int。所以,當我們需要一個接受一個double作為引數,返回int的物件時,就可以寫作:std::function<int(double)>[3]^{[3]}[3]。

1.4呼叫方式

Lambda表示式的呼叫方式與普通函式的呼叫方式相同,上面Lambda表示式的呼叫方式如下:

cout<<f()<<endl;  //列印42

//或者直接呼叫
cout<<[]{return 42;}()<<endl;

我們還可以定義一個單引數的Lambda,實現上面字串排序的shorter()比較函式的功能:

auto f=[](cosnt string& a,const string& b)
{
 return a.size()<b.size();
}

//將Lambda傳入排序演算法sort中
sort(words.begin(),word2.end(),[](cosnt string& a,const string& b){
 return a.size()<b.size();
});

//或者
sort(words.begin(),f);

2.Lambda的捕獲列表

Lambda可以獲取(捕獲)它所在作用域中的變數值,由捕獲列表(capture list)指定在Lambda 表示式的程式碼內可使用的外部變數。比如雖然一個Lambda可以出現在一個函式中,使用其區域性變數,但它只能使用那些在捕獲列表中明確指明的變數。Lambda在捕獲所需的外部變數有兩種方式:引用和值。我們可以在捕獲列表中設定各變數的捕獲方式。如果沒有設定捕獲列表,Lambda預設不能捕獲任何的變數。捕獲方式具體有如下幾種:

  • [] 不擷取任何變數
  • [&} 擷取外部作用域中所有變數,並作為引用在函式體中使用
  • [=] 擷取外部作用域中所有變數,並拷貝一份在函式體中使用
  • [=,&valist] 擷取外部作用域中所有變數,並拷貝一份在函式體中使用,但是對以逗號分隔valist使用引用
  • [&,valist] 以引用的方式捕獲外部作用域中所有變數,對以逗號分隔的變數列表valist使用值的方式捕獲
  • [valist] 對以逗號分隔的變數列表valist使用值的方式捕獲
  • [&valist] 對以逗號分隔的變數列表valist使用引用的方式捕獲
  • [this] 擷取當前類中的this指標。如果已經使用了&或者=就預設新增此選項。

在[]中設定捕獲列表,就可以在Lambda中使用變數a了,這裡使用按值(=, by value)捕獲。

#include <iostream>

int main()
{
 int a = 123;
 auto lambda = [=]()->void
 {
 std::cout << "In Lambda: " << a << std::endl;
 };
 lambda();
 return 0;
}

編譯執行結果如下:

In Lambda: 123

按值傳遞到Lambda中的變數,預設是不可變的(immutable),如果需要在Lambda中進行修改的話,需要在形參列表後新增mutable關鍵字(按值傳遞無法改變Lambda外變數的值)。

#include <iostream>
int main()
{
 int a = 123;
 std::cout << a << std::endl;
 auto lambda = [=]() mutable ->void{
 a = 234;
 std::cout << "In Lambda: " << a << std::endl;
 };
 lambda();
 std::cout << a << std::endl;
 return 0;
}

編譯執行結果為:

123
In Lambda: 234 //可以修改
123 //注意這裡的值,並沒有改變

如果沒有新增mutable,則編譯出錯:

$ g++ main.cpp -std=c++11
main.cpp:9:5: error: cannot assign to a variable captured by copy in a non-mutable Lambda
a = 234;
~ ^
1 error generated.

看到這,不禁要問,這魔法般的變數捕獲是怎麼實現的呢?原來,Lambda是通過建立個類來實現的。這個類過載了操作符(),一個Lambda函式是該類的一個例項。當該類被構造時,周圍的變數就傳遞給建構函式並以成員變數儲存起來,看起來跟函式物件(仿函式)很相似,但是C++11標準建議使用Lambda表示式,而不是函式物件,Lambda表示式更加輕量高效,易於使用和理解[4]^{[4]}[4]。

3.Lambda的型別

lambda函式的型別看起來和函式指標很像,都是把函式賦值給了一個變數。實際上,lambda函式是用仿函式實現的,它看起來又像是一種自定義的類。而事實上,lambda型別並不是簡單的函式指標型別或者自定義型別,lambda函式是一個閉包(closure)的類,C++11標準規定,closure型別是特有的、匿名且非聯合體的class型別。每個lambda表示式都會產生一個閉包型別的臨時物件(右值)。因此,嚴格來說,lambda函式並非函式指標,但是C++11允許lambda表示式向函式指標轉換,前提是沒有捕捉任何變數且函式指標所指向的函式必須跟lambda函式有相同的呼叫方式。

typedef int(*pfunc)(int x,int y);

int main()
{
 auto func = [](int x,int y)->int {
 return x + y;
 };
 pfunc p1 = nullptr;
 p1 = func;  //lambda表示式向函式指標轉換

 std::cout << p1(1,2) << std::endl;

 return 0;
}

4.lambda的常量性和mutable關鍵字

C++11中,預設情況下lambda函式是一個const函式,按照規則,一個const成員函式是不能在函式體內改變非靜態成員變數的值。

int main()
{
 int val = 0;
 auto const_val_lambda = [=] { val = 3; }; // 編譯失敗,不能在const的lambda函式中修改按值捕獲的變數val

 auto mutable_val_lambda = [=]() mutable { val = 3; };

 auto const_ref_lambda = [&] { val = 3; };

 auto const_param_lambda = [](int v) { v = 3; };
 const_param_lambda(val);

 return 0;
}

閱讀程式碼,注意以下幾點:
(1)可以看到在const的lambda函式中無法修改按值捕捉到的變數。lambda函式是通過仿函式來實現的,捕捉到的變數相當於是仿函式類中的成員變數,而lambda函式相當於是成員函式,const成員函式自然不能修改普通成員變數;
(2)使用引用的方式捕獲的變數在常量成員函式中值被更改則不會導致錯誤,其原因簡單地說,由於const_ref_lambda 不會改變引用本身,而只會改變引用的值,所以編譯通過;
(3)使用mutable修飾的mutable_val_lambda,去除了const屬性,所以可以修改按值方式捕獲到的變數;
(4)按值傳遞引數的const_param_lambda修改的是傳入lambda函式的實參,當然不會有問題。

5.Lambda的常見用法

(1)Lambda函式和STL
Lambda函式的引入為STL的使用提供了極大的方便。比如下面這個例子,當你想遍歷一個vector的時候,原來你得這麼寫:

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

//傳統的for迴圈
for ( auto itr = v.begin(),end = v.end(); itr != end; itr++ )
{ 
 cout << *itr; 
}

//函式指標
void printFunc(int v)
{
 cout<<v;
}
for_each(v.begin(),v.end(),printFunc);

//仿函式
struct CPrintFunc
{
 void operator() (int val)const { cout << val; }
};
for_each(v.begin(),CPrintFunc());

現在有了Lambda函式你就可以這麼寫:

for_each(v.begin(),[](int val)
{ 
 cout << val;
});

很明顯,相比於傳統的for迴圈、函式指標和仿函式,使用lambda函式更加簡潔。如果處理vector成員的業務程式碼更加複雜,那麼更能凸顯Lambda函式的便捷。而且這麼寫之後執行效率反而會提高,因為編譯器有可能使用迴圈展開來加速執行過程。

以上就是一文讀懂c++11 Lambda表示式的詳細內容,更多關於c++11 Lambda表示式的資料請關注我們其它相關文章!