【1】lambda表達式語法
lambda表達式的語法定義如下:
[capture](parameters)mutable ->return-type { statement };
(1)[capture]: 捕捉列表。捕捉列表總是出現在lambda函數的開始處。實質上,[]是lambda引出符(即獨特的標志符)
編譯器根據該引出符判斷接下來的代碼是否是lambda函數
捕捉列表能夠捕捉上下文中的變量以供lambda函數使用
捕捉列表由一個或多個捕捉項組成,並以逗號分隔,捕捉列表一般有以下幾種形式:
<1> [var] 表示值傳遞方式捕捉變量var
<2> [=] 表示值傳遞方式捕捉所有父作用域的變量(包括this指針)
<3> [&var] 表示引用傳遞捕捉變量var
<4> [&] 表示引用傳遞捕捉所有父作用域的變量(包括this指針)
<5> [this] 表示值傳遞方式捕捉當前的this指針
<6> [=,&a,&b] 表示以引用傳遞的方式捕捉變量 a 和 b,而以值傳遞方式捕捉其他所有的變量
<7> [&,a,this] 表示以值傳遞的方式捕捉 a 和 this,而以引用傳遞方式捕捉其他所有變量
備注:父作用域是指包含lambda函數的語句塊
另外,需要注意的是,捕捉列表不允許變量重復傳遞。下面的例子就是典型的重復,會導致編譯錯誤:
[=, a] 這里 = 已經以值傳遞方式捕捉了所有的變量,那么再捕捉 a 屬於重復
[&,&this] 這里 & 已經以引用傳遞方式捕捉了所有變量,那么再捕捉 this 屬於重復
(2)(parameters): 參數列表。與普通函數的參數列表一致。如果不需要參數傳遞,則可以連同括號()一起省略
(3)mutable : mutable修飾符。默認情況下,lambda函數總是一個const函數,mutable可以取消其常量性(后面有詳解)
在使用該修飾符時,參數列表不可省略(即使參數為空)
(4)->return-type : 返回類型。用追蹤返回類型形式聲明函數的返回類型。
出於方便,不需要返回值的時候也可以連同符號->一起省略
此外,在返回類型明確的情況下,也可以省略該部分,讓編譯器對返回類型進行推導
(5){statement} : 函數體。內容與普通函數一樣,不過除了可以使用參數之外,還可以使用所有捕獲的變量
在lambda函數的定義中,參數列表和返回類型都是可選的部分,而捕捉列表和函數體都可能為空
那么,在極端情況下,C++11中最為簡單的lambda函數只需要聲明為:
[]{};
就可以了。不過顯然,這樣的lambda函數不能做任何事情(乍一看好漂亮,其實僅是好看)。
【2】lambda函數示例代碼
示例代碼1:
1 #include <iostream>
2 using namespace std; 3
4 void main() 5 { 6 int a = 20, b = 10; 7
8 auto totalAB = [] (int x, int y)->int { return x + y; }; 9 int aAddb = totalAB(a, b); 10 cout << "aAddb :" << aAddb << endl; 11
12 auto totalAB2 = [a, &b]()->int { return a + b; }; 13 int aAddb2 = totalAB2(); 14 cout << "aAddb2 :" << aAddb2 << endl; 15
16 auto totalAB3 = [=]()->int { return a + b; }; 17 int aAddb3 = totalAB3(); 18 cout << "aAddb3 :" << aAddb3 << endl; 19
20 []{}; // 最簡lambda函數
21 [=] { return a + b; }; // 省略了參數列表與返回類型,返回類型由編譯器推斷為int
22 auto fun1 = [&] (int c) { b = a + c; }; // 省略了返回類型,無返回值
23 auto fun2 = [=, &b](int c)->int { return b += a + c; }; // 各部分都很完整的lambda函數
24 cout << "fun2(100) :" << fun2(100) << endl; 25 } 26 // Result:
27 /*
28 aAddb :30 29 aAddb2 :30 30 aAddb3 :30 31 fun2(100) :130 32 */
以上代碼僅供學習參考
【3】lambda函數的作用
lambda函數的使用示例代碼:
1 #include <iostream>
2 #include <vector>
3 #include <algorithm>
4 #include "time.h"
5 using namespace std; 6
7 void main() 8 { 9 vector<int> nVec; 10 for (int i = 0; i < 100000; ++i) 11 { 12 nVec.push_back(i); 13 } 14
15 double time_Start = (double)clock(); 16 for (vector<int>::const_iterator it = nVec.begin(); it != nVec.end(); ++it) 17 { 18 cout << *it << endl; 19 } 20 double time_Finish = (double)clock(); 21 double time_Interval_1 = (double)(time_Finish - time_Start) / 1000; 22
23 time_Start = (double)clock(); 24 for_each( nVec.begin(), nVec.end(), [] (int val){ cout << val << endl; } ); 25 time_Finish = (double)clock(); 26 double time_Interval_2 = (double)(time_Finish - time_Start) / 1000; 27
28 cout << "time_Interval_1 :" << time_Interval_1 << endl; 29 cout << "time_Interval_2 :" << time_Interval_2 << endl; 30
31 } 32 // Result:
33 /*
34 time_Interval_1 :17.748 35 time_Interval_2 :17.513 36 */
lambda函數的引入為STL的使用提供了極大的方便。同樣是遍歷容器,效率反而提高了很多。
【4】lambda函數 與 仿函數
何謂仿函數?個人理解,像函數一樣工作的對象。
根據面向對象的編程思想,那么問題來了!既然主語是一個對象,創建這個對象的類長什么樣子呢?
據聽說,所有科學中數學學科最重要,語文重要性次之。為什么呢?
數學可以利用來解決問題,但當問題解決不了的時候,可以用語文塗畫,塗畫得讓人聽不懂。好像很高大上一樣一樣~
關於仿函數,請看下面示例:
1 #include <iostream>
2 using namespace std; 3
4 class _functor_plus 5 { 6 private: 7 int m_nValue; 8
9 public: 10 _functor_plus(int nValue = 100); 11 _functor_plus operator+ (const _functor_plus & funObj); 12 void printInfo(); 13 }; 14
15 _functor_plus::_functor_plus(int nValue) : m_nValue(nValue) 16 { 17 } 18
19 _functor_plus _functor_plus::operator+ (const _functor_plus & funObj) 20 { 21 m_nValue += funObj.m_nValue; 22 return _functor_plus(m_nValue); 23 } 24
25 void _functor_plus::printInfo() 26 { 27 cout << m_nValue << endl; 28 } 29
30 class _functor_override 31 { 32 public: 33 int operator()(int x, int y); 34 }; 35
36 int _functor_override::operator()(int x, int y) 37 { 38 return x + y; 39 } 40
41 int main() 42 { 43 _functor_plus plusA, plusB, plusC; 44 plusC = plusA + plusB; 45 plusA.printInfo(); 46 plusB.printInfo(); 47 plusC.printInfo(); 48
49 int boys = 4, girls = 3; 50 _functor_override totalChildren; 51 cout << "totalChildren(int, int): " << totalChildren(boys, girls); 52 } 53 // Result:
54 /*
55 200 56 100 57 200 58 totalChildren(int, int): 7 59 */
在這個例子中,_functor_override類的operator()被重載。
因此,在調用該函數的時候,我們看到與函數調用一樣的形式。
只不過這里的totalChildren不是函數名稱,而是一個對象名稱。
相比於函數,仿函數可以擁有初始化狀態:
一般通過class定義私有成員,並在聲明對象的時候對其進行初始化,
那般,私有成員的狀態就成了仿函數的初始狀態。
由於聲明一個仿函數對象可以擁有多個不同的初始狀態的實例,
因此,可以借由仿函數產生多個功能類似實質卻各不同的仿函數實例。
請參見下例:
1 #include <iostream>
2 using namespace std; 3
4 class Tax 5 { 6 private: 7 double m_dRate; 8 int m_nBase; 9
10 public: 11 Tax(double dRate, int nBase) : m_dRate(dRate), m_nBase(nBase) 12 { 13 } 14
15 double operator() (double dMoney) 16 { 17 return (dMoney - m_nBase) * m_dRate; 18 } 19 }; 20
21 int main() 22 { 23 Tax high(0.40, 30000); 24 Tax middle(0.25, 20000); 25 cout << "tax over 3w: " << high(37500) << endl; 26 cout << "tax over 2w: " << middle(24000) << endl; 27 } 28 // Result:
29 /*
30 tax over 3w: 3000 31 tax over 2w: 1000 32 */
到這里,是否發現仿函數和lambda之間存在一種“衍生”的關系?
難道還不明顯?沒看懂?咱再接着剖析,誰讓程序員就這么理性呢?
請再看下例:
1 #include <iostream>
2 using namespace std; 3
4 class AirportPrice 5 { 6 private: 7 double m_dDutyfreeRate; 8
9 public: 10 AirportPrice(double dDutyfreeRate) : m_dDutyfreeRate(dDutyfreeRate) 11 {} 12
13 double operator() (double dPrice) 14 { 15 return dPrice * (1 - m_dDutyfreeRate/100); 16 } 17 }; 18
19 void main() 20 { 21 double dRate = 5.5; 22 AirportPrice fanFunObj(dRate); 23
24 auto ChangLambda = [dRate](double dPrice)->double
25 { 26 return dPrice * (1 - dRate/100); 27 }; 28 double purchased1 = fanFunObj(3699); 29 double purchased2 = ChangLambda(3699); 30 cout << "purchased1:" << purchased1 << endl; 31 cout << "purchased2:" << purchased2 << endl; 32 } 33 // Result:
34 /*
35 purchased1:3495.55 36 purchased2:3495.55 37 */
分別使用了仿函數和lambda兩種方式來完成扣稅后的產品價格計算。
lamba函數捕捉了dRate變量,而仿函數則以dRate進行初始化類。
其他的,在參數傳遞上,兩者保持一致,結果也一致。
可以看到,除去在語法層面的差異,lambda函數和仿函數有着相同的內涵:
即都可以捕捉一些變量作為初始化狀態,並接受參數進行運算。
而事實上,仿函數正是編譯器實現lambda的一種方式。
在現階段,通常編譯器都會把lambda函數轉化為一個仿函數對象。
因此,C++11中,lambda可以視為仿函數的一種等價形式。
備注:有時,編譯時發現lambda函數出現了錯誤,編譯器會提示一些構造函數相關的信息,
顯然是由於lambda的這種實現方式造成的。理解這種實現也能夠正確理解錯誤信息的由來。
【5】lambda函數等同於一個局部函數
局部函數,在函數作用域中定義的函數,也稱為內嵌函數。
局部函數通常僅屬於其父作用域,能夠訪問父作用域的變量。
C/C++語言標准中不允許局部函數存在(FORTRAN語言支持)
C++11標准卻用比較優雅的方式打破了這個規則。
因為事實上,lambda可以像局部函數一樣使用。請參見下例:
1 #include <iostream>
2 using namespace std; 3
4 extern int z = 100; 5 extern float c = 100.00; 6
7 void Calc(int& rnOne, int nTwo, float& rfThree, float fFour) 8 { 9 rnOne = nTwo; 10 rfThree = fFour; 11 } 12
13 void TestCalc() 14 { 15 int x, y = 3; 16 float a, b = 4.0; 17 int success = 0; 18
19 auto validate = [&]()->bool
20 { 21 if ((x == y + z) && (a == b + c)) 22 return 1; 23 else
24 return 0; 25 }; 26
27 Calc(x, y, a, b); 28 success += validate(); 29
30 y = 1024; 31 b = 100.0; 32 Calc(x, y, a, b); 33 success += validate(); 34 } 35
36 void main() 37 { 38 }
在沒有lambd函數之前,通常需要在TestCalc外聲明同樣一個函數,
並且把TestCalc中的變量當作參數進行傳遞。
出於函數作用域及運行效率的考慮,那樣聲明函數通常要加上關鍵字static 和 inline
相比於一個傳統意義上的函數定義,lambda函數在這里直觀,使用方便可讀性很好。請參見下例:
1 #include <iostream>
2 using namespace std; 3
4 int Prioritize(int nValue) 5 { 6 return nValue + 10; 7 } 8
9 int AllWorks(int nTimes) 10 { 11 int i = 0, x = 0; 12 try
13 { 14 for (i = 0; i < nTimes; ++i) 15 { 16 x += Prioritize(i); 17 } 18 } 19 catch (...) 20 { 21 x = 0; 22 } 23
24 const int y = [=]()->int
25 { 26 int i = 0, val = 0; 27 try
28 { 29 for (; i < nTimes; ++i) 30 { 31 val += Prioritize(i); 32 } 33 } 34 catch (...) 35 { 36 val = 0; 37 } 38 return val; 39 }(); 40 // lambda表達式
41 { 42 []{}(); 43 [](){}(); 44 []{ cout << "emptyLambdaExec" << endl; }(); 45 [=](){ cout << "const int y :" << y << endl; }(); 46 [&](){ cout << "int x :" << x << endl; }(); 47 } 48
49 return 0; 50 } 51
52 void main() 53 { 54 AllWorks(10); 55 } 56
57 // Result:
58 /*
59 emptyLambdaExec 60 const int y :145 61 int x :145 62 */
備注:注意此例中的lambda表達式作用域中比較特殊的幾個lambda函數。
【6】關於lambda的一些問題及其有趣的測試
(1)使用lambda函數時候,不同的捕捉方式會導致不同的結果:
請看下例:
1 #include <iostream>
2 using namespace std; 3
4 void main() 5 { 6 int j = 10; 7 auto by_val_lambda = [=] { return j + 1; }; 8 auto by_ref_lambda = [&] { return j + 1; }; 9 cout << "by_val_lambda: " << by_val_lambda() << endl; 10 cout << "by_ref_lambda: " << by_ref_lambda() << endl; 11 ++j; 12 cout << "by_val_lambda: " << by_val_lambda() << endl; 13 cout << "by_ref_lambda: " << by_ref_lambda() << endl; 14 } 15
16 //Result:
17 /*
18 by_val_lambda: 11 19 by_ref_lambda: 11 20 by_val_lambda: 11 21 by_ref_lambda: 12 22 */
充分說明了傳值和引用方式的區別。
(2)使用lambda函數與函數指針
一般情況下,把匿名的lambda函數賦值給一個auto類型的變量,
這是一種聲明和使用lambda函數的方法。
結合關於auto的知識,有人會猜測totalChild是一種函數指針類型的變量
結合lambda函數和仿函數之間關系,大多人會傾向於認為lambda是一種自定義類型。
實質上,lambda的類型並非簡單函數指針類型或自定義類型。
從C++11標准定義發現,lambda類型被定義為“閉包”的類,而每一個lambda表達式則會產生一個閉包類型的臨時對象。
也因此,嚴格地講,lambda函數並非函數指針。
但是,C++11標准卻允許lambda表達式向函數指針的轉換,
前提是lambda函數沒有捕捉任何變量,且函數指針所示的函數原型,必須跟lambda函數有着相同的調用方式。
1 #include <iostream>
2 using namespace std; 3
4 void main() 5 { 6 int girs = 3, boys = 4; 7 auto totalChild = [](int x, int y)->int{ return x + y; }; 8 typedef int (*pFunAll)(int x, int y); 9 typedef int (*pFunOne)(int x); 10
11 pFunAll funAll; 12 // funAll = totalChild; // 編譯失敗!
13
14 pFunOne funOne; 15 // funOne = totalChild; //編譯失敗!參數必須一致
16
17 decltype(totalChild) allPeople = totalChild; // 需通過decltype獲得lambda的類型 18 // decltype(totalChild) totalPeople = funAll; // 編譯失敗,指針無法轉換lambda
19 }
第 12 行,編譯錯誤信息如下:
error C2440: “=”: 無法從“`anonymous-namespace'::<lambda0>”轉換為“pFunAll”
沒有可用於執行該轉換的用戶定義的轉換運算符,或者無法調用該運算符
MSVC10環境下,第一步編譯不通過。
關於此問題參見文章《在 MSVC10 下,將 lambda expression 轉換成 C 的 function pointer》
第 15 行 編譯失敗,參數不一致
第 18 行 編譯失敗,函數指針轉換為lambda也是不成功的。
值得注意的是,可以通過decltype的方式獲取lambda函數的類型。
(3)lambda函數的常量性以及mutable關鍵字
C++11中,默認情況下lambda函數是一個const函數。
神馬意思呢?請參見下例:
1 #include <iostream>
2 using namespace std; 3
4 class const_val_lambda 5 { 6 public: 7 const_val_lambda(int v) : m_nVal(v) 8 {} 9 public: 10 void operator() () const
11 { 12 // m_nVal = 3; /*注意:常量成員函數*/
13 } 14
15 void ref_const_Fun(int& nValue) const
16 { 17 nValue = 100; 18 } 19
20 private: 21 int m_nVal; 22 }; 23
24 void main() 25 { 26 int val = 10; 27 // 編譯失敗!在const的lambda中修改常量 28 // auto const_val_lambda = [=]() { val = 3;}; // 不能在非可變 lambda 中修改按值捕獲 29 // 非const的lambda,可以修改常量數據
30 auto mutable_val_lambda = [=]() mutable{ val = 3; }; 31 // 依然是const的lambda,不過沒有改動引用本身
32 auto const_ref_lambda = [&] { val = 3; }; 33 // 依然是const的lambda,通過參數傳遞val
34 auto const_param_lambda = [&](int varA) { varA = 3;}; 35 const_param_lambda(val); 36 }
備注:使用引用方式傳遞的變量在常量成員函數中修改值並不會導致錯誤。
【7】lambda 與 STL
lambda對C++11最大的貢獻,或者說改變,應該在STL庫中
相關應用,具體請再參見下例:
1 #include <vector>
2 #include <algorithm>
3 #include <iostream>
4 using namespace std; 5
6 const int ubound = 3; 7
8 vector<int> nums; 9 vector<int> largeNums; 10
11 void initNums() 12 { 13 for (int i = 1; i < 5; ++i) 14 { 15 nums.push_back(i); 16 } 17 } 18
19 inline void largeNumsFunc(int i) 20 { 21 if (i > ubound) 22 { 23 largeNums.push_back(i); 24 } 25 } 26
27 void filter() 28 { 29 for (auto it = nums.begin(); it != nums.end(); ++it) 30 { 31 if ((*it) > ubound) 32 { 33 largeNums.push_back(*it); 34 } 35 } 36
37 for_each (nums.begin(), nums.end(), largeNumsFunc); 38
39 for_each (nums.begin(), nums.end(), [=](int i) 40 { 41 if (i > ubound) 42 { 43 largeNums.push_back(i); 44 } 45 }); 46 } 47
48 void printInfo() 49 { 50 for_each (largeNums.begin(), largeNums.end(), [=](int i) 51 { 52 cout << i << " "; 53 }); 54 cout << endl; 55 } 56
57 void main() 58 { 59 initNums(); // 初始化值
60 filter(); // 過濾值
61 printInfo(); //打印信息
62 }
具體遇到其它的問題 ,再具體分析和學習。
【8】在返回類型明確的情況下,可以省略返回值類型,讓編譯器對返回類型進行推導
第一部分第4小節:“此外,在返回類型明確的情況下,也可以省略該部分,讓編譯器對返回類型進行推導”,示例如下:
1 #include <iostream> 2 #include <string> 3 4 int main() 5 { 6 std::string str = "user_behavior_log"; 7 auto key = [&]() { 8 if (str.empty()) 9 { 10 return std::string{}; 11 } 12 13 return (str + ".json"); 14 }; 15 16 std::cout << key() << std::endl; 17 18 return 0; 19 } 20 21 // result 22 /* 23 user_behavior_log.json 24 */
希望能加深對返回類型的理解。
【9】lambda函數的總結
C++11中的Lambda表達式用於定義並創建匿名的函數對象,以簡化編程工作。
Good Good Study, Day Day Up.
順序 選擇 循環 總結