Lambda
lambda表達式是C++11標准引入的新特性之一,它的名字來自於大名鼎鼎的$\lambda$演算。百度百科這樣介紹:
$\lambda$演算(英語:lambda calculus,LC)是一套從數學邏輯中發展,以變量綁定和替換的規則,來研究函數如何抽象化定義、函數如何被應用以及遞歸的形式系統。它由數學家阿隆佐·邱奇在20世紀30年代首次發表。lambda演算作為一種廣泛用途的計算模型,可以清晰地定義什么是一個可計算函數,而任何可計算函數都能以這種形式表達和求值,它能模擬單一磁帶圖靈機的計算過程;盡管如此,lambda演算強調的是變換規則的運用,而非實現它們的具體機器。
有關LC更詳細的解讀,推薦知乎的這篇:什么是 Lambda 演算?的幾個高分回答,用以了解足矣。
本文,主要講解C++中lambda表達式的相關內容。
lambda 表達式的意義
C++11標准引入的lambda表達式在大多數情況下被看作是一個語法糖,它的使用場景多為一次性的,編寫函數對象類。lambda表達式可以很方便地在原地編寫一個匿名函數類型的對象,並將其作為另一個函數的參數進行傳遞。
一個典型的lambda表達式常用於封裝幾行代碼再傳遞給算法或者異步方法。一個典型的例子就是 std::sort() 函數中傳遞的第三個參數:
#include <algorithm> #include <cmath> void abssort(float* x, unsigned n) { std::sort(x, x + n, // Lambda expression begins [](float a, float b) { return (std::abs(a) < std::abs(b)); } // end of lambda expression ); }
使用 Lambda 表達式可以減少程序中函數對象類的數量,使得程序更加優雅。
優勢
1)遵守就近原則:隨時定義隨時使用,lambda表達式的定義和使用在同一個地方,並且lambda表達式可以直接在其他函數中定義使用,其他函數沒有這個優勢。
2)簡潔明了:lambda表達式相比較其他函數的定義和使用更加簡潔明了一些。
3)效率相對較高:lambda表達式不會阻止編譯器的內聯,而函數指針則會阻止編譯器內聯。
4)捕獲動態變量:lambda表達式可以捕獲它可以訪問的作用域內(父作用域以及全局作用域)的任何動態變量。
Lambda表達式的語法
lambda表達式的語法定義如下:
[ capture clause ] ( parameter list ) mutable / exception -> return type { function body }
- capture clause:捕捉列表。標識一個Lambda表達式的開始,這部分必須存在,不能省略。[]是Lambda引出符。編譯器根據該引出符判斷接下來的代碼是否是Lambda函數。捕捉列表能夠捕捉上下文中的變量以供Lambda函數使用;
- parameter list:參數列表。與普通函數的參數列表一致。如果不需要參數傳遞,則可以連同括號“()”一起省略。從C++14開始,支持默認參數,並且參數列表中如果使用 auto 的話,該lambda稱為泛化lambda(generic lambda);
- mutable:mutable修飾符。默認情況下,Lambda函數總是一個const函數,mutable可以取消其常量性。在使用該修飾符時,參數列表不可省略(即使參數為空);
- -> 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
捕獲列表
(Also known as the lambda-introducer in the C++ specification)
lambda可以定義在函數內部,使用其局部變量,但它只能使用那些明確指明的變量。lambda通過將外部函數的局部變量包含在其捕獲列表中來指出將會使用這些變量。
當定義一個lambda時,編譯器生成一個與lambda對應的新的(未命名的)類。當向函數傳遞一個lambda時,同時定義了一個新類型和該類型的一個對象:傳遞的參數就是編譯器生成的類類型的未命名對象;類似的,當使用auto定義一個用lambda初始化的變量時,定義了一個從lambda生成的類型的對象。
默認情況下,從lambda生成的類都包含一個對應該lambda所捕獲的變量的數據成員。類似任何普通類的數據成員,lambda的數據成員在lambda對象創建時被初始化。
值捕獲
類似參數傳遞,變量的捕獲方式可以是值或引用。與傳值參數類似,采用值捕獲的前提是變量可以拷貝。被捕獲的變量的值是在lambda創建時拷貝,而不是調用時拷貝:
int v1 = 42; auto f=[v1]{return v1;}; v1=0; auto j = f(); //j is 42
由於被捕獲變量的值是在lambda創建時拷貝,因此隨后對其修改不會影響到lambda內對應的值。
引用捕獲
定義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也不能包含引用捕獲。
隱式捕獲
除了顯式列出我們希望使用的來自所在函數的變量之外,還可以讓編譯器根據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所在類的this),並且是值傳遞方式(相當於編譯器自動為我們按值傳遞了所有局部變量)。
- [&]:函數體內可以使用lambda所在范圍內(備注)所有可見的局部變量(包括lambda所在類的this),並且是引用傳遞方式(相當於是編譯器自動為我們按引用傳遞了所有局部變量)。
- [this]:函數體內可以使用lambda所在類中的成員變量。
- [a]:將變量a按值進行傳遞。按值進行傳遞時,函數體內不能修改傳遞進來的a的拷貝,因為默認情況下函數是const的,要修改傳遞進來的拷貝,可以添加 mutable 修飾符。
- [&a]:將變量a按引用進行傳遞。
- [a, &b]:將變量a按值傳遞,變量b按引用進行傳遞。
- [=, &a, &b]:除變量a和變量b按引用進行傳遞外,其他參數都按值進行傳遞。
- [&, a, b]:除變量a和變量b按值進行傳遞外,其他參數都按引用進行傳遞。
【備注】有效范圍為lambda所在父作用域,即包含lambda函數的語句塊,通俗點就是包含lambda的最小“{}”代碼塊。另,全局變量不用在捕獲列表中聲明,可在函數體內直接使用。
參數列表
標識重載的 operator() 操作符的參數,沒有參數時,這部分可以省略。參數可以通過按值 (int a, int b) 和按引用 (int &a, int &b) 兩種方式進行傳遞。
mutable / exception
這部分可以省略。按值傳遞函數對象參數時,加上 mutable 修飾符后,可以修改傳遞進來的拷貝(注意是能修改拷貝,而不是值本身)。 exception 聲明用於指定函數拋出的異常,如拋出整數類型的異常,可以使用 throw(int) 。
mutable示例
默認情況下,對於一個按值捕獲的變量,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
More about Exception
推薦微軟的這篇:Exception specifications (throw, noexcept) (C++)作為擴展閱讀。
-> 返回值類型
即尾置返回值類型(trailing return type)。【關於trailing return type的更多內容,推薦模板函數——后置返回值類型(trailing return type)和C++11新特性:尾置返回類型快速了解】
標識函數返回值的類型,當返回值為void,或者函數體中只有一處return的地方(此時編譯器可以自動推斷出返回值類型)時,這部分可以省略。
函數體
標識函數的實現,這部分不能省略,但函數體可以為空。
示例與總結
[] (int x, int y) { return x + y; } // 隱式返回類型 [] (int& x) { ++x; } // 沒有 return 語句 -> Lambda 函數的返回類型是 'void' [] () { ++global_x; } // 沒有參數,僅訪問某個全局變量 [] { ++global_x; } // 與上一個相同,省略了 (操作符重載函數參數)
可以像下面這樣顯示指定返回類型:
[] (int x, int y) -> int { int z = x + y; return z; }
在這個例子中創建了一個臨時變量 z 來存儲中間值。和普通函數一樣,這個中間值不會保存到下次調用。什么也不返回的lambda函數可以省略返回類型,而不需要使用 -> void 形式。lambda函數可以引用在它之外聲明的變量. 這些變量的集合叫做一個閉包. 閉包被定義在 Lambda 表達式聲明中的方括號 [] 內。這個機制允許這些變量被按值或按引用捕獲。如下圖的例子:
示例1
std::vector<int> some_list; int total = 0; for (int i = 0; i < 5; ++i) some_list.push_back(i); std::for_each(begin(some_list), end(some_list), [&total](int x) { total += x; });
此例計算 list 中所有元素的總和。變量 total 被存為 Lambda 函數閉包的一部分。因為它是棧變量(局部變量)total 引用,所以可以改變它的值。
示例2
std::vector<int> some_list; int total = 0; int value = 5; std::for_each(begin(some_list), end(some_list), [&, value, this](int x) { total += x * value * this->some_func(); });
此例中 total 會存為引用, value 則會存一份值拷貝。對this的捕獲比較特殊,它只能按值捕獲。this只有當包含它的最靠近它的函數不是靜態成員函數時才能被捕獲。對protect和private成員來說,這個lambda函數與創建它的成員函數有相同的訪問控制。如果this被捕獲了,不管是顯式還是隱式的,那么它的類的作用域對lambda函數就是可見的。訪問this的成員不必使用 this-> 語法,可以直接訪問。
總結
不同編譯器的具體實現可以有所不同,但期望的結果是: 按引用捕獲的任何變量,lambda 函數實際存儲的應該是這些變量在創建這個lambda函數的函數的棧指針,而不是lambda函數本身棧變量的引用。不管怎樣,因為大多數lambda函數都很小且在局部作用中,與候選的內聯函數很類似,所以按引用捕獲的那些變量不需要額外的存儲空間。如果一個閉包含有局部變量的引用,在超出創建它的作用域之外的地方被使用的話,這種行為是未定義的!lambda函數是一個依賴於實現的函數對象類型,這個類型的名字只有編譯器知道. 如果用戶想把lambda函數做為一個參數來傳遞, 那么形參的類型必須是模板類型或者必須能創建一個 std::function 類似的對象去捕獲lambda 函數。使用 auto 關鍵字可以幫助存儲lambda函數:
auto my_lambda_func = [&](int x) { /* ... */ }; auto my_onheap_lambda_func = new auto([=](int x) { /* ... */ });
這里有一個例子, 把匿名函數存儲在變量、數組或 vector 中,並把它們當做命名參數來傳遞:
一個沒有指定任何捕獲的 lambda 函數,可以顯式轉換成一個具有相同聲明形式函數指針.所以,像下面這樣做是合法的:
auto a_lambda_func = [](int x) { /* ... */ }; void (*func_ptr)(int) = a_lambda_func; func_ptr(4); // calls the lambda
更多lambda示例詳見微軟的這篇:Examples of Lambda Expressions
參考資料
https://docs.microsoft.com/en-us/cpp/cpp/lambda-expressions-in-cpp?view=msvc-160
https://www.cnblogs.com/gqtcgq/p/9939651.html
https://blog.csdn.net/u010984552/article/details/53634513
https://blog.csdn.net/u010984552/article/details/53634513