您好,登錄后才能下訂單哦!
這篇文章給大家介紹如何在C++項目中書寫Lambda表達式,內(nèi)容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
一、什么是Lambda表達式
MSDN上對lambda表達式的解釋:
在 C++ 11 中,lambda 表達式(通常稱為 “l(fā)ambda”)是一種在被調(diào)用的位置或作為參數(shù)傳遞給函數(shù)的位置定義匿名函數(shù)對象的簡便方法。 Lambda 通常用于封裝傳遞給算法或異步方法的少量代碼行。 [1]
看了這個解釋,相信大家已經(jīng)理解lambda表達式是什么。簡而言之,lambda表達式就是一種定義函數(shù)的簡單的方法。
舉一個簡單的例子:求一個數(shù)的階乘。
這是一般的函數(shù)的寫法:
// 這里要求n>=0,同時n的取值不能太大,會溢出 // 為了方便,這里并沒有處理上面說到的問題 int factorial(int n) { int fact = 1; for (int i = 1; i <= n; ++ i) fact *= i; return fact; }
Lambda表達式的寫法:
autofactorial = [](int n) { int fact = 1; for (int i = 1; i <= n; ++ i) fact *= i; return fact; };
乍一看,這兩種定義方式十分的相似。但其實這是兩種完全不同的方式,前一種是函數(shù)定義式,而后一種是一個表達式。factorial是變量名,等于號后面的是值,也就是一個lambda表達式,本質(zhì)上是一個匿名的函數(shù)。最終factorial就是一個函數(shù)。
很多時候,我們只是直接書寫lambda表達式,而不需要給他一個名字。比如排序的時候,sort可以接受一個自定義的比較函數(shù),這時候直接書寫lambda表達式即可。
二、Lambda表達式的作用
由于lambda本身其實也就是一種函數(shù)的定義方式。因此它的主要作用還是和一般函數(shù)一樣。但是lambda表達式相對于一般函數(shù),又有一些功能之外的作用。參考了知乎上的一些回答 [2] ,小喵也進行了總結(jié)。
1、可以用表達式來定義函數(shù),這樣使得函數(shù)的定義和調(diào)用在一起,語意和邏輯上更為緊湊。同時,對于只是用一次的短小的函數(shù),直接調(diào)用匿名的lambda表達式是最好的選擇,這樣就不需要給每個函數(shù)起名字了。 /* 起名字一直是一個很令人頭疼的問題 */
2、閉包(Closure)。這個小喵的寫javascript的時候時常會用到。閉包本質(zhì)上就是能夠訪問上下文環(huán)境中變量的代碼塊。
這里我們簡單的舉個例子,還是之前的求階乘的問題,現(xiàn)在我們有些提高需求。
現(xiàn)在需要完成下面的三種階乘的運算:
n! = n * (n – 1) * (n – 2) * …
n!! = n * (n – 2) * (n – 4) * …
n!!! = n * (n – 3) * (n – 6) * …
要求編寫3個函數(shù),分別完成上述3種計算。
使用一般的方式寫很容易實現(xiàn),我們這里直接使用lambda表達式來實現(xiàn):
#include <iostream> #include <functional> std::function<int(int)> getFactorialFunc(int n) { return [n](int x) { int fact = 1; for (; x >= 1; x -= n) fact *= x; return fact; }; } int main() { // 構(gòu)造要求的三個函數(shù) autofactorial1 = getFactorialFunc(1); autofactorial2 = getFactorialFunc(2); autofactorial3 = getFactorialFunc(3); // 調(diào)用 std::cout << factorial1(10) << std::endl; std::cout << factorial2(10) << std::endl; std::cout << factorial3(10) << std::endl; }
編譯的時候要注意,lambda表達式是C++11開始支持的,所以需要指定一下C++的版本。
g++ factorial_lambda.cpp -o factorial_lambda.out --std=c++11
運行之后的結(jié)果為:
./factorial_lambda.out
3628800
3840
280
這里作為返回值的lambda表達式,可以訪問先前傳入的參數(shù),這也就是閉包。具體的語法,我們后面會講到。
3、柯里化(Currying)。這部分小喵也是第一次接觸,維基百科有如下解釋:
在計算機科學中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。 [3]
下面給出一個例子(也是實現(xiàn)之前的階乘):
#include <iostream> #include <functional> // 兩個參數(shù)的階乘 int factorial(int n, int step) { int r = 1; for (; n >= 1; n -= step) { r *= n; } return r; } // curring化的階乘 std::function<int(int)> currying_factorial(int step) { return [step](int n) { return factorial(n, step); }; } int main() { // 調(diào)用普通函數(shù) std::cout << factorial(10, 1) << std::endl; std::cout << factorial(10, 2) << std::endl; std::cout << factorial(10, 3) << std::endl; // 調(diào)用currying函數(shù) std::cout << currying_factorial(1)(10) << std::endl; std::cout << currying_factorial(2)(10) << std::endl; std::cout << currying_factorial(3)(10) << std::endl; return 0; }
4、lambda表達式整體可以被當做函數(shù)的參數(shù)或者返回值。
閉包和currying的例子就是將整個lambda表達式作為返回值?,F(xiàn)在再舉一個作為參數(shù)的例子:
#include <iostream> #include <functional> int operate(int x, int y, const std::function<int(int, int)> &op) { return op(x, y); } int main() { autoadd = [](int x, int y) { return x + y;}; automul = [](int x, int y) { return x - y;}; std::cout << operate(10, 5, add) << std::endl; std::cout << operate(10, 5, mul) << std::endl; return 0; }
運行的結(jié)果:
其實函數(shù)也可以當參數(shù)傳入的(函數(shù)指針),但是lambda表達式要更為直觀和靈活一些。誰能一眼看出int (*func(int))(int)究竟是什么意思呢(這是一個函數(shù)的定義,輸入的參數(shù)是int,返回值是一個函數(shù)指針,函數(shù)指針對應(yīng)的函數(shù)的輸入和輸出類型都是int)。
三、Lambda表達式的語法
看到前面的lambda表達式的各種有趣的功能,現(xiàn)在是不是非常迫切的想嘗試一把?
ISO C++ 標準展示了作為第三個參數(shù)傳遞給 std::sort() 函數(shù)的簡單 lambda:
#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表達式的組成部分見下圖:
Capture 子句(在 C++ 規(guī)范中也稱為 lambda 引導(dǎo)。)
參數(shù)列表(可選)。 (也稱為 lambda 聲明符)
可變規(guī)范(可選)。
異常規(guī)范(可選)。
尾隨返回類型(可選)。
“l(fā)ambda 體”
接下來我們需要學習這6個部分。
1、Capture 子句
我們知道,一般情況下,函數(shù)只能訪問自己的參數(shù)和外部的全局變量。而lambda表達式卻可以訪問上下文的變量(參見閉包的例子)。那么如何指定要訪問的變量,以及訪問的方式(值或者引用)呢?這就是Capture 子句要解決的問題。
Lambda 可在其主體中引入新的變量(用 C++14),它還可以訪問(或 “捕獲” )周邊范圍內(nèi)的變量。 Lambda 以 Capture 子句(標準語法中的 lambda 引導(dǎo) )開頭,它指定要捕獲的變量以及是通過值還是引用進行捕獲。 有與號 ( & ) 前綴的變量通過引用訪問,沒有該前綴的變量通過值訪問。
空 capture 子句 [ ] 指示 lambda 表達式的主體不訪問封閉范圍中的變量。
可以使用默認捕獲模式(標準語法中的 capture-default )來指示如何捕獲 lambda 中引用的任何外部變量:[&] 表示通過引用捕獲引用的所有變量,而 [=] 表示通過值捕獲它們。 可以使用默認捕獲模式,然后為特定變量顯式指定相反的模式。 例如,如果 lambda 體通過引用訪問外部變量 total 并通過值訪問外部變量 factor ,則以下 capture 子句等效:
[&total, factor]
[factor, &total]
[&, factor]
[factor, &]
[=, &total]
[&total, =]
我們之前的閉包中使用的就是通過值訪問。
使用 capture-default 時,只有 lambda 中提及的變量才會被捕獲。
如果 capture 子句包含 capture-default & ,則該 capture 子句的 identifier 中沒有任何 capture 可采用 & identifier 形式。 同樣,如果 capture 子句包含 capture-default = ,則該 capture 子句的 capture 不能采用 = identifier 形式。 identifier 或 this 在 capture 子句中出現(xiàn)的次數(shù)不能超過一次。 以下代碼片段給出了一些示例。
struct S { void f(int i); }; void S::f(int i) { [&, i]{}; // OK [=, &i]{}; // OK [&, &i]{}; // ERROR: i preceded by & when & is the default [=, this]{}; // ERROR: this when = is the default [i, i]{}; // ERROR: i repeated }
capture 后跟省略號是包擴展,如以下可變參數(shù)模板 [4] 示例中所示:
template<class... Args> void f(Args... args) { auto x = [args...] { return g(args...); }; x(); }
要在類方法的正文中使用 lambda 表達式,需要將this指針傳遞給 Capture 子句,以提供對封閉類的方法和數(shù)據(jù)成員的訪問權(quán)限。
這里大家可能覺得有點奇怪,將this指針傳給Capture子句?
其實我們常使用的成員函數(shù)也是用類似的方法實現(xiàn)的。我們知道,使用成員函數(shù)需要有一個類實例,但是調(diào)用類函數(shù)就不需要。這是因為成員函數(shù)的第一個參數(shù)是this,當然這個參數(shù)我們編寫代碼的時候不需要自己手動寫出,而是默認的。使用像python這樣的語言的時候就是需要顯示的寫出的。在使用類實例調(diào)用成員函數(shù)的時候,會默認將this指針傳入。成員函數(shù)有這么一個參數(shù),就可以訪問類實例的各種變量和方法。而類函數(shù)是沒有這個參數(shù)的,也就是沒有this這個指針,因此它的調(diào)用并不需要類實例,當然也就不能訪問類實例的變量。
在使用 capture 子句時,要記住以下幾點(尤其是使用采取多線程的 lambda 時):
引用捕獲可用于修改外部變量,而值捕獲卻不能實現(xiàn)此操作。 (mutable允許修改副本,而不能修改原始項。)
引用捕獲會反映外部變量的更新,而值捕獲卻不會反映。
引用捕獲引入生存期依賴項,而值捕獲卻沒有生存期依賴項。 當 lambda 以異步方式運行時,這一點尤其重要。 如果在異步 lambda 中通過引用捕獲本地變量,該本地變量將很可能在 lambda 運行時消失,從而導(dǎo)致運行時訪問沖突。
通用捕獲 (C++14)
在 C++14 中,可在 Capture 子句中引入并初始化新的變量,而無需使這些變量存在于 lambda 函數(shù)的封閉范圍內(nèi)。 初始化可以任何任意表達式表示;且將從該表達式生成的類型推導(dǎo)新變量的類型。 此功能的一個好處是,在 C++14 中,可從周邊范圍捕獲只移動的變量(例如 std::unique_ptr)并在 lambda 中使用它們。
pNums = make_unique<vector<int>>(nums); //... auto a = [ptr = move(pNums)]() { // use ptr };
2、參數(shù)列表
除了捕獲變量,lambda 還可接受輸入?yún)?shù)。 參數(shù)列表(在標準語法中稱為 lambda 聲明符 )是可選的,它在大多數(shù)方面類似于函數(shù)的參數(shù)列表。
autoadd = [] (int first, int second) { return first + second; };
在 C++14 中,如果參數(shù)類型是泛型,則可以使用 auto 關(guān)鍵字作為類型說明符。 這將告知編譯器將函數(shù)調(diào)用運算符創(chuàng)建為模板。 參數(shù)列表中的每個 auto 實例等效于一個不同的類型參數(shù)。
autoadd = [] (autofirst, autosecond) { return first + second; };
lambda 表達式可以將另一個 lambda 表達式作為其參數(shù)。
由于參數(shù)列表是可選的,因此在不將參數(shù)傳遞到 lambda 表達式,并且其 lambda-declarator: 不包含 exception-specification 、 trailing-return-type 或 mutable 的情況下,可以省略空括號。
[]{}; // 這就是最簡單的lambda表達式
3、可變規(guī)范
通常,lambda 的函數(shù)調(diào)用運算符為 const-by-value,但對 mutable 關(guān)鍵字的使用可將其取消。 它不會生成可變的數(shù)據(jù)成員。 利用可變規(guī)范,lambda 表達式的主體可以修改通過值捕獲的變量。 本文后面的一些示例將顯示如何使用 mutable 。
#include <iostream> int main() { int n = 10; autolambda1 = [n](int x) { /* ++ n; */ // 這句編譯會出錯,錯誤信息如下: // error: cannot assign to a variable captured // by copy in a non-mutable lambda return x + n; }; autolambda2 = [n](int x) mutable { ++ n; return x + n; }; std::cout << lambda1(5) << " " << n << std::endl; std::cout << lambda2(5) << " " << n << std::endl; return 0; }
輸出的結(jié)果是:
可以看出n確實是通過值來訪問,在lambda1中,我們運行++n,在編譯的時候會報錯。使用mutable修飾之后,就可以修改參數(shù)(副本)的值。
4、異常規(guī)范
你可以使用 throw() 異常規(guī)范來指示 lambda 表達式不會引發(fā)任何異常。與普通函數(shù)一樣,如果 lambda 表達式聲明 C4297 異常規(guī)范且 lambda 體引發(fā)異常,Visual C++ 編譯器將生成警告 throw() ,如下所示:
// throw_lambda_expression.cpp // compile with: /W4 /EHsc int main() // C4297 expected { []() throw() { throw 5; }(); }
在MSDN的異常規(guī)范 [5] 中,明確指出異常規(guī)范是在 C++11 中棄用的 C++ 語言功能。因此這里不建議不建議大家使用。
5、返回類型
將自動推導(dǎo) lambda 表達式的返回類型。 無需使用 auto 關(guān)鍵字,除非指定 尾隨返回類型 。 trailing-return-type 類似于普通方法或函數(shù)的返回類型部分。 但是,返回類型必須跟在參數(shù)列表的后面,你必須在返回類型前面包含 trailing-return-type 關(guān)鍵字 -> 。
如果 lambda 體僅包含一個返回語句或其表達式不返回值,則可以省略 lambda 表達式的返回類型部分。 如果 lambda 體包含單個返回語句,編譯器將從返回表達式的類型推導(dǎo)返回類型。 否則,編譯器會將返回類型推導(dǎo)為 void 。
#include <iostream> #include <typeinfo> int main() { autolambda1 = [](int i) {return i;}; autolambda2 = [](int i) -> bool {return i;}; autolambda3 = [](int i) -> float {return i;}; /* auto lambda4 = []{ return {1, 2}; };*/ // ERROR: return type is void // cannot deduce lambda return type autox1 = lambda1(10); autox2 = lambda2(10); autox3 = lambda3(10); std::cout << x1 << " " << typeid(x1).name() << std::endl; std::cout << x2 << " " << typeid(x2).name() << std::endl; std::cout << x3 << " " << typeid(x3).name() << std::endl; return 0; }
typeinfo的功能是獲取一個變量的類型,由于它的實現(xiàn)依賴于編譯器,所以在不同平臺下的輸出可能不完全一樣。小喵這邊的輸出是:
10 i
1 b
10 f
可以看出,三個lambda的輸出是不相同的。默認情況下,會返回一個最直接的類型。
6、lambda體
lambda體其實和函數(shù)體幾乎完全相同。
lambda 表達式的 lambda 體(標準語法中的 compound-statement )可包含普通方法或函數(shù)的主體可包含的任何內(nèi)容。 普通函數(shù)和 lambda 表達式的主體均可訪問以下變量類型:
從封閉范圍捕獲變量,如前所述(Capture)。
參數(shù)
本地聲明變量
類數(shù)據(jù)成員(在類內(nèi)部聲明并且捕獲 this 時)
具有靜態(tài)存儲持續(xù)時間的任何變量(例如,全局變量)
這里要注意我們在Capture 規(guī)范中說到的值訪問和引用訪問的特點。
下面的例子都是MSDN上給出的。
以下示例包含通過值顯式捕獲變量 n 并通過引用隱式捕獲變量 m 的 lambda 表達式:
// captures_lambda_expression.cpp // compile with: /W4 /EHsc #include <iostream> using namespace std; int main() { int m = 0; int n = 0; [&, n] (int a) mutable { m = ++n + a; }(4); cout << m << endl << n << endl; }
輸出結(jié)果:
5
0
由于變量 n 是通過值捕獲的,因此在調(diào)用 lambda 表達式后,變量的值仍保持 0 不變。 mutable 規(guī)范允許在 lambda 中修改 n 。
盡管 lambda 表達式只能捕獲具有自動存儲持續(xù)時間的變量,但你可以在 lambda 表達式的主體中使用具有靜態(tài)存儲持續(xù)時間的變量。 以下示例使用 generate 函數(shù)和 lambda 表達式為 vector 對象中的每個元素賦值。 lambda 表達式將修改靜態(tài)變量以生成下一個元素的值。
void fillVector(vector<int>& v) { // A local static variable. static int nextValue = 1; // The lambda expression that appears in the following call to // the generate function modifies and uses the local static // variable nextValue. generate(v.begin(), v.end(), [] { return nextValue++; }); //WARNING: this is not thread-safe and is shown for illustration only }
四、應(yīng)用Lambda的比較函數(shù)的編寫
為什么要補充這一部分呢?因為我們在寫程序的時候,往往最常用到lambda的地方就是數(shù)組的sort。
首先,我們知道std::sort默認是接受2個參數(shù)的,表示需要排序的序列的開始和結(jié)尾。對于一些復(fù)雜的數(shù)據(jù)類型,我們可以給它添加一個用來比較的函數(shù) operator <。但更多的是通過給sort添加第三個參數(shù)來實現(xiàn)。而這個參數(shù)就是一個比較器。
sort默認使用<比較符來進行比較,排序的結(jié)果是升序。我們寫的比較函數(shù)的功能就是代替<。記住這個特點,就不會在編寫比較函數(shù)的時候理不清思路。
這里舉一個小例子,給一組點坐標,按歐氏距離排序:
#include <algorithm> #include <iostream> #include <vector> using namespace std; int main() { vector< pair<int, int> > arr; arr.push_back(make_pair(1, 4)); arr.push_back(make_pair(2, 3)); arr.push_back(make_pair(5, 7)); arr.push_back(make_pair(6, 2)); sort(arr.begin(), arr.end(), [](pair<int, int> left, pair<int, int> right) { int d1 = left.first * left.first + left.second * left.second; int d2 = right.first * right.first + right.second * right.second; return d1 < d2; }); for (auto &p: arr) { cout << "(" << p.first << ", " << p.second << ")" << endl; } return 0; }
輸出結(jié)果:
(2, 3)
(1, 4)
(6, 2)
(5, 7)
唯一需要注意的是,我們的比較函數(shù)取代的是<。
PS:
lambda 怎么傳遞ref參數(shù)
ambda 傳遞ref參數(shù)有個語法bug,必須要顯式書寫參數(shù)類型。
//如 delegate bool FuncType(ref int num); FuncType func1; func1 = num => true; //錯 func1 = (ref num) => true;//錯 func1 = (ref int num) => true;//ok //并且,當一個參數(shù)書寫類型,其他參數(shù)也要書寫,總之很煩。
關(guān)于如何在C++項目中書寫Lambda表達式就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。