C++11—lambda函數


【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.

順序 選擇 循環 總結


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM