C++ lambda表達式總結
一個lambda表達式用於創建閉包。lambda表達式與任何函數類似,具有返回類型、參數列表和函數體。與函數不同的是,lambda能定義在函數內部。lambda表達式具有如下形式
[ capture list ] ( parameter list) -> return type { function body }
capture list,捕獲列表,是一個lambda所在函數中定義的局部變量的列表。lambda函數體中可以使用這些局部變量。捕獲可以分為按值捕獲和按引用捕獲。非局部變量,如靜態變量、全局變量等可以不經捕獲,直接使用;
parameter list,參數列表。從C++14開始,支持默認參數,並且參數列表中如果使用auto的話,該lambda稱為泛化lambda(generic lambda);
return type,返回類型,這裏使用了返回值類型尾序語法(trailing return type synax)。可以省略,這種情況下根據lambda函數體中的return語句推斷出返回類型,就像普通函數使用decltype(auto)推導返回值類型一樣;如果函數體中沒有return,則返回類型為void。
function body,與任何普通函數一樣,表示函數體。
Lambda表達式可以忽略參數列表和返回類型,但必須包含捕獲列表和函數體:
auto f = [] { return 42; } cout << f() << endl;
上面的lambda表達式,定義了一個可調用對象f,它不接受參數,返回42。Lambda的調用方式與普通函數的調用方式相同。
lambda表達式是用於生成閉包的純右值(prvalue)表達式。每一個lambda表達式都定義了獨一無二的閉包類,閉包類內主要的成員有operator()成員函數:
ret operator()(params) const { body } //the keyword mutable was not used ret operator()(params) { body } //the keyword mutable was used template<template-params> //since C++14, generic lambda ret operator()(params) const { body } template<template-params> //since C++14, generic lambda, the keyword mutable was used ret operator()(params) { body }
當調用lambda表達式生成的閉包時,執行operator()函數。除非lambda表達式中使用了mutable關鍵字,否則lambda生成的閉包類的operator()函數具有const飾詞,從而lambda函數體中不能修改其按值捕獲的變量;如果lambda表達式的參數列表中使用了auto,則相應的參數稱為模板成員函數operator()的模板形參,該lambda表達式也就成了泛化lambda表達式。
如果捕獲列表中,有按值捕獲的局部變量,則閉包類中就會有相應的未命名成員變量副本,這些成員變量在定義lambda表達式時就由那些相應的局部變量進行初始化。如果按值捕獲的變量是個函數引用,則相應的成員變量是引用指向函數的左值引用;如果是個對象引用,則相應的成員變量是該引用指向的對象。如果是按引用捕獲,標準中未指明是否會在閉包類中引入相應的成員變量。
該閉包類還有其他成員函數。比如轉換為函數指針的轉換函數、構造函數(包括復制構造函數)、析構函數等,具體可參考https://en.cppreference.com/w/cpp/language/lambda
一:捕獲列表
lambda可以定義在函數內部,使用其局部變量,但它只能使用那些明確指明的變量。lambda通過將外部函數的局部變量包含在其捕獲列表中來指出將會使用這些變量。
當定義一個lambda時,編譯器生成一個與lambda對應的新的(未命名的)類。當向函數傳遞一個lambda時,同時定義了一個新類型和該類型的一個對象:傳遞的參數就是編譯器生成的類類型的未命名對象;類似的,當使用auto定義一個用lambda初始化的變量時,定義了一個從lambda生成的類型的對象。
默認情況下,從lambda生成的類都包含一個對應該lambda所捕獲的變量的數據成員。類似任何普通類的數據成員,lambda的數據成員在lambda對象創建時被初始化。
1:值捕獲
類似參數傳遞,變量的捕獲方式可以是值或引用。與傳值參數類似,采用值捕獲的前提是變量可以拷貝。被捕獲的變量的值是在lambda創建時拷貝,而不是調用時拷貝:
int v1 = 42; auto f=[v1]{return v1;}; v1=0; auto j = f(); //j is 42
由於被捕獲變量的值是在lambda創建時拷貝,因此隨後對其修改不會影響到lambda內對應的值。
2:引用捕獲
定義lambda時可以采用引用方式捕獲變量。例如:
int v1 = 42; auto f=[&v1]{return v1;}; v1=0; auto j = f(); //j is 0
v1之前的&指出v1應該以引用方式捕獲。一個以引用方式捕獲的變量與其他任何類型的引用的行為類似。當我們在lambda函數體內使用此變量時,實際上使用的是引用所綁定的對象。在本例中,當lambda返回v1時,它返回的是v1指向的對象的值。
引用捕獲與返回引用有著相同的問題和限制。如果我們采用引用方式捕獲一個變量,就必須確保被引用的對象在lambda執行的時候是存在的。lambda捕獲的都是局部變量,這些變量在函數結束後就不復存在了。如果lambda可能在函數結束後執行,捕獲的引用指向的局部變量已經消失,這就是未定義行為。
引用捕獲有時是必要的:
void biggies(vector<string> &words, vector<string>::size_ type sz, ostream &os=cout, char c=‘ ‘) { for_each(words.begin(), words.end(), [&os, c](const strinq &s) { os << s << c; }); }
不能拷貝ostream對象,因此捕獲os的唯一方法就是捕獲其引用。當我們向一個函數傳遞lambda時,就像本例子調用for_each那樣,lambda會在函數內部執行。在此情況下,以引用方式捕獲os沒有問題,因為當for_each執行時,biggies中的變量是存在的。
我們也可以從一個函數返回lambda。函數可以直接返問一個可調用對象,或者返回一個類對象,該類含有可調用對象的數據成員。如果函數返回一個lambda,則與函數不能返回一個局部變量的引用類似,此lambda也不能包含引用捕獲。
3:隱式捕獲
除了顯式列出我們希望使用的來自所在函數的變量之外,還可以讓編譯器根據lambda體中的代碼來推斷我們要使用哪些變量。為了指示編譯器推斷捕獲列表,應在捕獲列表中寫一個” &” 或”=”。 ” &”告訴編譯器采用引用捕獲方式,”=”則表示采用值捕獲方式。例如:
we = find_if(words.begin(), words.end(), [=](const string &s) { return s.size() >= sz; });
如果希望對一部分變量采用值捕獲,對其他變量采用引用捕獲,可以混合使用隱式捕獲和顯式捕獲:
void biggies(vector<string> &words, vector<string>::size_ type sz, ostream &os=cout, char c=‘ ‘) { //os隱式捕獲,引用捕獲方式;c顯式捕獲,值捕獲方式 for_each(words.begin(), words.end(), [&, c](const strinq &s) { os << s << c; }); //os顯式捕獲,引用捕獲方式;c隱式捕獲,值捕獲方式 for_each(words.begin(), words.end(), [=, &os](const strinq &s) { os << s << c; }); }
當混合使用隱式捕獲和顯式捕獲時,捕獲列表中的第一個元素必須是一個”&”或”=“。此符號指定了默認捕獲方式為引用或值;並且顯式捕獲的變量必須使用與隱式捕獲不同的方式。即,如果隱式捕獲是引用方式,則顯式捕獲命名變量必須采用值方式;類似的,如果隱式捕獲采用的是值方式,則顯式捕獲命名變量必須采用引用方式。
二:可變lambda
默認情況下,對於一個按值捕獲的變量,lambda不能改變其值。如果希望能改變這個被捕獲的變量的值,就必須在參數列表之後加上關鍵字mutable,因此,可變lambda不能省略參數列表:
int v1 = 42; auto f=[v1] () mutable {return ++v1;}; v1=0; auto j = f(); //j is 43
一個引用捕獲的變量是否可以修改依賴於此引用指向的是一個const類型還是一個非const類型:
int v1 = 42; auto f=[&v1] () {return ++v1;}; v1=0; auto j = f(); //j is 1
C++ lambda表達式總結