C++ 11中關於Lambda表達式(匿名函數)

C++ 98/03

C++ 98/03標準並不原生支持匿名函數。不過可以利用Boost庫的Boost.Lambda來實現一個匿名函數。


C++11

初步瞭解:

很多語言都提供了 lambda 表達式,如 Python,Java 8。lambda 表達式可以方便地構造匿名函數,如果你的代碼裏面存在大量的小函數,而這些函數一般只被調用一次,那麼不妨將他們重構成 lambda 表達式。

C++11 的 lambda 表達式規範如下:

[ capture ](params )mutableexception attribute -> ret{body } (1)  
[ capture ](params )->ret {body} (2)  
[ capture ](params ){body } (3)  
[ capture ]{body } (4)  

其中

  • (1) 是完整的 lambda 表達式形式,
  • (2) const 類型的 lambda 表達式,該類型的表達式不能改捕獲("capture")列表中的值。
  • (3)省略了返回值類型的 lambda 表達式,但是該 lambda 表達式的返回類型可以按照下列規則推演出來:
    • 如果 lambda 代碼塊中包含了 return 語句,則該 lambda 表達式的返回類型由 return 語句的返回類型確定。
    • 如果沒有 return 語句,則類似 void f(...) 函數。
  • 省略了參數列表,類似於無參函數 f()。

mutable 修飾符說明 lambda 表達式體內的代碼可以修改被捕獲的變量,並且可以訪問被捕獲對象的 non-const 方法。

exception 說明 lambda 表達式是否拋出異常(noexcept),以及拋出何種異常,類似於void f()throw(X, Y)

attribute 用來聲明屬性。

另外,capture 指定了在可見域範圍內 lambda 表達式的代碼內可見得外部變量的列表,具體解釋如下:

  • [a,&b] a變量以值的方式唄捕獲,b以引用的方式被捕獲。
  • [this] 以值的方式捕獲 this 指針。
  • [&] 以引用的方式捕獲所有的外部自動變量。
  • [=] 以值的方式捕獲所有的外部自動變量。
  • [] 不捕獲外部的任何變量。

             1、空。沒有使用任何函數對象參數。
           2、=。函數體內可以使用Lambda所在作用範圍內所有可見的局部變量(包括Lambda所在類的this),並且是值傳遞方式(相當於編譯器自動爲我們按值傳遞了所有局部變量)。
           3、&。函數體內可以使用Lambda所在作用範圍內所有可見的局部變量(包括Lambda所在類的this),並且是引用傳遞方式(相當於編譯器自動爲我們按引用傳遞了所有局部變量)。
           4、this。函數體內可以使用Lambda所在類中的成員變量。
           5、a。將a按值進行傳遞。按值進行傳遞時,函數體內不能修改傳遞進來的a的拷貝,因爲默認情況下函數是const的。要修改傳遞進來的a的拷貝,可以添加mutable修飾符。
           6、&a。將a按引用進行傳遞。
           7、a, &b。將a按值進行傳遞,b按引用進行傳遞。
           8、=,&a, &b。除a和b按引用進行傳遞外,其他參數都按值進行傳遞。
           9、&, a, b。除a和b按值進行傳遞外,其他參數都按引用進行傳遞。

此外,params 指定 lambda 表達式的參數。


詳解:

C++11標準提供了匿名函數的支持,在《ISO/IEC 14882:2011》(C++11標準文檔)中叫做lambda表達式[10]。一個lambda表達式有如下的形式:

[capture] (parameters) mutable exception attribute -> return_type { body }

必須用方括號括起來的capture列表來開始一個lambda表達式的定義。

lambda函數的形參表比普通函數的形參表多了3條限制:

  1. 參數不能有缺省值
  2. 不能有可變長參數列表
  3. 不能有無名參數

如果lambda函數沒有形參且沒有mutable、exception或attribute聲明,那麼參數的空圓括號可以省略。但如果需要給出mutable、exception或attribute聲明,那麼參數即使爲空,圓括號也不能省略。

如果函數體只有一個return語句,或者返回值類型爲void,那麼返回值類型聲明可以被省略:

[capture](parameters){body}

一個lambda函數的例子如下:

[](int x, int y) { return x + y; } // 從return語句中隱式獲得的返回值類型
[](int& x) { ++x; }   // 沒有return語句 -> lambda函數的返回值為void
[]() { ++global_x; }  // 沒有參數,僅僅是訪問一個全局變量
[]{ ++global_x; }     // 與前者相同,()可以被省略

這個無名函數的返回值是decltype(x+y) (在上面的第一個例子中)。如果lambda函數體的形式是returnexpression,或者什麼也每返回,或者所有返回語句用decltype都能檢測到同一類型,那麼返回值類型可以被省略。

返回值類型可以顯式指定,如下所示:

[](int x, int y) -> int { int z = x + y; return z; }

在這個例子中,一個臨時變量,z,被創建來儲存中間過程。與一般的函數一樣,中間值在調用的前後並不存在。什麼也沒有返回的lambda表達式無需顯式指定返回值,沒有必要寫-> void代碼。

lambda函數可以捕獲lambda函數外的具有automatic storage duration的變量。函數體與這些變量的集合合起來稱做閉包。這些外部變量在聲明lambda表達式時列在在方括號[]中。空的方括號表示沒有外界變量被capture。這些變量被傳值捕獲或者引用捕獲。對於傳值捕獲的變量,默認爲只讀。修改這些變量將導致編譯報錯。但在lambda表達式的參數表的圓括號後面使用mutable關鍵字,就允許lambda函數體內的語句修改傳值引用的變量,這些修改與lambda表達式(實際上是用函數對象實現)有相同的生命期,但不影響被傳值捕獲的外部變量的值。lambda函數可以直接使用具有static存儲期的變量。如果在lambda函數的捕獲列表中給出了static存儲期的變量,編譯時會給出警告,仍然按照lambda函數直接使用這些外部變量來處理。因此具有static存儲期的變量即使被聲明爲傳值捕獲,修改該變量實際上直接修改了這些外部變量。編譯器生成lambda函數對應的函數對象時,不會用函數對象的數據成員來保持被“捕獲”的static存儲期的變量。示例:

[]        // 沒有定義任何變量,但必須列出空的方括號。在Lambda表達式中嘗試使用任何外部變量都會導致編譯錯誤。
[x, &y]   // x是按值傳遞,y是按引用傳遞
[&]       // 任何被使用到的外部變量都按引用傳入。
[=]       // 任何被使用到的外部變量都按值傳入。
[&, x]    // x按值傳入。其它變量按引用傳入。
[=, &z]   // z按引用傳入。其它變量按值傳入。

下面這個例子展示了lambda表達式的使用:

std::vector<int> some_list{ 1, 2, 3, 4, 5 };
int total = 0;
std::for_each(begin(some_list), end(some_list), 
                 [&total](int x) {  total += x; }
              );

在類的非靜態成員函數中定義的lambda表達式可以顯式或隱式捕捉this指針,從而可以引用所在類對象的數據成員與函數成員。

lambda函數的函數體中,可以訪問下述變量:

  • 函數參數
  • 局部聲明的變量
  • 類數據成員(當函數聲明在類中)
  • 具有靜態存儲期的變量(如全局變量)
  • 被捕獲的外部變量
    • 顯式捕獲的變量
    • 隱式捕獲的變量,使用默認捕獲模式(傳值或引用)來訪問。

lambda函數的數據類型是函數對象,保存時必須用std::function模板類型或auto關鍵字,或用auto關鍵字。例如:

#include <functional>
#include <vector>
#include <iostream>
 
double eval(std::function <double(double)> f, double x = 2.0)
{
	return f(x);
}
 
int main()
{
	std::function<double(double)> f0    = [](double x){return 1;};
	auto                          f1    = [](double x){return x;};
	decltype(f0)                  fa[3] = {f0,f1,[](double x){return x*x;}};
	std::vector<decltype(f0)>     fv    = {f0,f1};
	fv.push_back                  ([](double x){return x*x;});
	for(int i=0;i<fv.size();i++)
		std::cout << fv[i](2.0) << std::endl;
	for(int i=0;i<3;i++)
		std::cout << fa[i](2.0) << std::endl;
	for(auto &f : fv)
		std::cout << f(2.0) << std::endl;
	for(auto &f : fa)
		std::cout << f(2.0) << std::endl;
	std::cout << eval(f0) << std::endl;
	std::cout << eval(f1) << std::endl;
	std::cout << eval([](double x){return x*x;}) << std::endl;
	return 0;
}

一個lambda函數的捕捉表達式爲空,則可以用普通函數指針存儲或調用。例如:

auto a_lambda_func = [](int x) { /*...*/ };
void (* func_ptr)(int) = a_lambda_func;
func_ptr(4); //calls the lambda.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章