C++11 lambda 表達式

#include<iostream>
using namespace std;
int main()
{
    int a = 1;
    int b = 2;

    auto func = [=, &b](int c)->int {return b += a + c;};
    return 0;
}ios

基本語法

簡單來講,Lambda函數也就是一個函數,它的語法定義以下:
[capture](parameters) mutable ->return-type{statement}
[capture]:捕捉列表。捕捉列表老是出如今Lambda函數的開始處。實際上,[]是Lambda引出符。編譯器根據該引出符判斷接下來的代碼是不是Lambda函數。捕捉列表可以捕捉上下文中的變量以供Lambda函數使用;
(parameters):參數列表。與普通函數的參數列表一致。若是不須要參數傳遞,則能夠連同括號「()」一塊兒省略;
mutable:mutable修飾符。默認狀況下,Lambda函數老是一個const函數,mutable能夠取消其常量性。在使用該修飾符時,參數列表不可省略(即便參數爲空);
->return-type:返回類型。用追蹤返回類型形式聲明函數的返回類型。咱們能夠在不須要返回值的時候也能夠連同符號」->」一塊兒省略。此外,在返回類型明確的狀況下,也能夠省略該部分,讓編譯器對返回類型進行推導;
{statement}:函數體。內容與普通函數同樣,不過除了可使用參數以外,還可使用全部捕獲的變量。
與普通函數最大的區別是,除了可使用參數之外,Lambda函數還能夠經過捕獲列表訪問一些上下文中的數據。具體地,捕捉列表描述了上下文中哪些 數據能夠被Lambda使用,以及使用方式(以值傳遞的方式或引用傳遞的方式)。語法上,在「[]」包括起來的是捕捉列表,捕捉列表由多個捕捉項組成,並 以逗號分隔。捕捉列表有如下幾種形式:

[var]表示值傳遞方式捕捉變量var;
[=]表示值傳遞方式捕捉全部父做用域的變量(包括this);
[&var]表示引用傳遞捕捉變量var;
[&]表示引用傳遞方式捕捉全部父做用域的變量(包括this);
[this]表示值傳遞方式捕捉當前的this指針。
上面提到了一個父做用域,也就是包含Lambda函數的語句塊,說通俗點就是包含Lambda的「{}」代碼塊。上面的捕捉列表還能夠進行組合,例如:

[=,&a,&b]表示以引用傳遞的方式捕捉變量a和b,以值傳遞方式捕捉其它全部變量;
[&,a,this]表示以值傳遞的方式捕捉變量a和this,引用傳遞方式捕捉其它全部變量。
不過值得注意的是,捕捉列表不容許變量重複傳遞。下面一些例子就是典型的重複,會致使編譯時期的錯誤。例如:

[=,a]這裏已經以值傳遞方式捕捉了全部變量,可是重複捕捉a了,會報錯的;
[&,&this]這裏&已經以引用傳遞方式捕捉了全部變量,再捕捉this也是一種重複。
Lambda的使用

對於Lambda的使用,說實話,我沒有什麼多說的,我的理解,在沒有Lambda以前的C++ , 咱們也是那樣好好的使用,並無對缺乏Lambda的C++有什麼抱怨,而如今有了Lambda表達式,只是更多的方便了咱們去寫代碼。不知道你們是否記 得C++ STL庫中的仿函數對象,仿函數想對於普通函數來講,仿函數能夠擁有初始化狀態,而這些初始化狀態是在聲明仿函數對象時,經過參數指定的,通常都是保存在 仿函數對象的私有變量中;在C++中,對於要求具備狀態的函數,咱們通常都是使用仿函數來實現,好比如下代碼:

#include<iostream>
using namespace std;
 
typedef enum
{
    add = 0,
    sub,
    mul,
    divi
}type;

class Calc
{
    public:
        Calc(int x, int y):m_x(x), m_y(y){}
 
        int operator()(type i)
        {
            switch (i)
            {
                case add:
                    return m_x + m_y;
                case sub:
                    return m_x - m_y;
                case mul:
                    return m_x * m_y;
                case divi:
                    return m_x / m_y;
            }
        }
 
    private:
        int m_x;
        int m_y;
};

int main()
{
    Calc addObj(10, 20);
    cout<<addObj(add)<<endl; // 發現C++11中,enum類型的使用也變了,更「強」了                                                                                                                                              
    return 0;
}
如今咱們有了Lambda這個利器,那是否是能夠重寫上面的實現呢?看代碼:

#include<iostream>
using namespace std;
      
typedef enum
{     
    add = 0,
    sub,
    mul,
    divi
}type;
      
int main()
{     
    int a = 10;
    int b = 20;
      
    auto func = [=](type i)->int {
        switch (i)
        {
            case add:
                return a + b;
            case sub:
                return a - b;
            case mul:
                return a * b;
            case divi:
                return a / b;
        }
    };
      
    cout<<func(add)<<endl;
}
顯而易見的效果,代碼簡單了,你也少寫了一些代碼,也去試一試C++中的Lambda表達式吧。

關於Lambda那些奇葩的東西

看如下一段代碼:

#include<iostream>         
using namespace std;       
                           
int main()                 
{                          
    int j = 10;            
    auto by_val_lambda = [=]{ return j + 1; };
    auto by_ref_lambda = [&]{ return j + 1; };
    cout<<"by_val_lambda: "<<by_val_lambda()<<endl;
    cout<<"by_ref_lambda: "<<by_ref_lambda()<<endl;
                           
    ++j;                   
    cout<<"by_val_lambda: "<<by_val_lambda()<<endl;
    cout<<"by_ref_lambda: "<<by_ref_lambda()<<endl;
                           
    return 0;              
}
程序輸出結果以下:

by_val_lambda: 11
by_ref_lambda: 11
by_val_lambda: 11
by_ref_lambda: 12
你想到了麼???那這又是爲何呢?爲何第三個輸出不是12呢?

在by_val_lambda中,j被視爲一個常量,一旦初始化後不會再改變(能夠認爲以後只是一個跟父做用域中j同名的常量),而在 by_ref_lambda中,j仍然在使用父做用域中的值。因此,在使用Lambda函數的時候,若是須要捕捉的值成爲Lambda函數的常量,咱們通 常會使用按值傳遞的方式捕捉;相反的,若是須要捕捉的值成成爲Lambda函數運行時的變量,則應該採用按引用方式進行捕捉。

再來一段更暈的代碼:

#include<iostream>                  
using namespace std;                
                                    
int main()                          
{                                   
    int val = 0;                                    
    // auto const_val_lambda = [=](){ val = 3; }; wrong!!!
                                    
    auto mutable_val_lambda = [=]() mutable{ val = 3; };
    mutable_val_lambda();           
    cout<<val<<endl; // 0
                                    
    auto const_ref_lambda = [&]() { val = 4; };
    const_ref_lambda();             
    cout<<val<<endl; // 4
                                    
    auto mutable_ref_lambda = [&]() mutable{ val = 5; };
    mutable_ref_lambda();           
    cout<<val<<endl; // 5
                                    
    return 0;      
}
這段代碼主要是用來理解Lambda表達式中的mutable關鍵字的。默認狀況下,Lambda函數老是一個const函數,mutable能夠 取消其常量性。按照規定,一個const的成員函數是不能在函數體內修改非靜態成員變量的值。例如上面的Lambda表達式能夠當作如下仿函數代碼:

class const_val_lambda
{
public:
    const_val_lambda(int v) : val(v) {}
    void operator()() const { val = 3; } // 常量成員函數

private:
    int val;
};
對於const的成員函數,修改非靜態的成員變量,因此就出錯了。而對於引用的傳遞方式,並不會改變引用自己,而只會改變引用的值,所以就不會報錯了。都是一些糾結的規則。慢慢理解吧。
總結
對於Lambda這種東西,有的人用的很是爽,而有的人看着都不爽。仁者見仁,智者見智。無論怎麼樣,做爲程序員的你,都要會的。這篇文章就是用來 彌補本身對C++ Lambda表達式的認知不足的過錯,以避免之後在別人的代碼中看到了Lambda,還看不懂這種東西,那就丟大人了。
程序員


爲何說 lambda 表達式如此激動人心呢?舉一個例子。標準 C++ 庫中有一個經常使用算法的庫,其中提供了不少算法函數,好比 sort() 和 find()。這些函數一般須要提供一個「謂詞函數 predicate function」。所謂謂詞函數,就是進行一個操做用的臨時函數。好比 find() 須要一個謂詞,用於查找元素知足的條件;可以知足謂詞函數的元素纔會被查找出來。這樣的謂詞函數,使用臨時的匿名函數,既能夠減小函數數量,又會讓代碼變 得清晰易讀。web

下面來看一個例子:算法

C++閉包

1函數

2this

3spa

4指針

5code

6

7

8

9

#include <algorithm>

#include <cmath>

 

void abssort(float *x, unsigned N)

{

  std::sort(x,

            x + N,

            [](float a, float b) { return std::abs(a) < std::abs(b); });

}

從上面的例子來看,儘管支持 lambda 表達式,但 C++ 的語法看起來卻很「神奇」。lambda 表達式使用一對方括號做爲開始的標識,相似於聲明一個函數,只不過這個函數沒有名字,也就是一個匿名函數。這個匿名函數接受兩個參數,ab; 其返回值是一個 bool 類型的值,注意,返回值是自動推斷的,不須要顯式聲明,不過這是有條件的!條件就是,lambda 表達式的語句只有一個 return。函數的做用是比較 a、b 的絕對值的大小。而後,在此例中,這個 lambda 表達式做爲一個閉包被傳遞給 std::sort() 函數。

下面,咱們來詳細解釋下這個神奇的語法到底表明着什麼。

咱們從另一個例子開始:

C++

1

std::cout << [](float f) { return std::abs(f); } (-3.5);

輸出值是什麼?3.5!注意,這是一個函數對象(由 lambda 表達式生成),其實參是 -3.5,返回值是參數的絕對值。lambda 表達式的返回值類型是語言自動推斷的,由於std::abs()的返回值就是 float。注意,前面咱們也提到了,只有當 lambda 表達式中的語句「足夠簡單」,才能自動推斷返回值類型。

C++ 11 的這種語法,其實就是匿名函數聲明以後立刻調用(不然的話,若是這個匿名函數既不調用,又不做爲閉包傳遞給其它函數,那麼這個匿名函數就沒有什麼用處)。若是你以爲奇怪,那麼來看看 JavaScript 的這種寫法:

JavaScript

1

2

3

function() {} ();

 

function(a) {} (-3.5);

C++ 11 的寫法徹底相似 JavaScript 的語法。

若是我不想讓 lambda 表達式自動推斷類型,或者是 lambda 表達式的內容很複雜,不能自動推斷怎麼辦?好比,std::abs(float)的返回值是 float,我想把它強制轉型爲 int。那麼,此時,咱們就必須顯式指定 lambda 表達式返回值的類型:

C++

1

std::cout << [](float f) -> int { return std::abs(f); } (-3.5);

這個語句與前面的不一樣之處在於,lambda 表達式的返回時不是 float 而是 int。也就是說,上面語句的輸出值是 3。返回值類型的概念同普通的函數返回值類型是徹底同樣的。

引入 lambda 表達式的前導符是一對方括號,稱爲 lambda 引入符(lambda-introducer)。lambda 引入符是有其本身的做用的,不只僅是代表一個 lambda 表達式的開始那麼簡單。lambda 表達式可使用與其相同範圍 scope 內的變量。這個引入符的做用就是代表,其後的 lambda 表達式以何種方式使用(正式的術語是「捕獲」)這些變量(這些變量可以在 lambda 表達式中被捕獲,其實就是構成了一個閉包)。目前爲止,咱們看到的僅僅是一個空的方括號,其實,這個引入符是至關靈活的。例如:

C++

1

2

float f0 = 1.0;

std::cout << [=](float f) { return f0 + std::abs(f); } (-3.5);

其輸出值是 4.5。[=] 意味着,lambda 表達式以傳值的形式捕獲同範圍內的變量。另一個例子:

C++

1

2

3

float f0 = 1.0;

std::cout << [&](float f) { return f0 += std::abs(f); } (-3.5);

std::cout << '\n' << f0 << '\n';

輸出值是 4.5 和 4.5。[&] 代表,lambda 表達式以傳引用的方式捕獲外部變量。那麼,下一個例子:

C++

1

2

3

float f0 = 1.0;

std::cout << [=](float f) mutable { return f0 += std::abs(f); } (-3.5);

std::cout << '\n' << f0 << '\n';

這個例子頗有趣。首先,[=]意味着,lambda 表達式以傳值的形式捕獲外部變量。C++ 11 標準說,若是以傳值的形式捕獲外部變量,那麼,lambda 體不容許修改外部變量,對 f0 的任何修改都會引起編譯錯誤。可是,注意,咱們在 lambda 表達式前聲明瞭mutable關 鍵字,這就容許了 lambda 表達式體修改 f0 的值。所以,咱們的例子本應報錯,可是因爲有 mutable 關鍵字,則不會報錯。那麼,你會以爲輸出值是什麼呢?答案是,4.5 和 1.0。爲何 f0 仍是 1.0?由於咱們是傳值的,雖然在 lambda 表達式中對 f0 有了修改,但因爲是傳值的,外部的 f0 依然不會被修改。

上面的例子是,全部的變量要麼傳值,要麼傳引用。那麼,是否是有混合機制呢?固然也有!好比下面的例子:

C++

1

2

3

4

float f0 = 1.0f;

float f1 = 10.0f;

std::cout << [=, &f0](float a) { return f0 += f1 + std::abs(a); } (-3.5);

std::cout << '\n' << f0 << '\n';

這個例子的輸出是 14.5 和 14.5。在這個例子中,f0 經過引用被捕獲,而其它變量,好比 f1 則是經過值被捕獲。

下面咱們來總結下全部出現的 lambda 引入符:

  • []        // 不捕獲任何外部變量

  • [=]      // 以值的形式捕獲全部外部變量

  • [&]      // 以引用形式捕獲全部外部變量

  • [x, &y] // x 以傳值形式捕獲,y 以引用形式捕獲

  • [=, &z]// z 以引用形式捕獲,其他變量以傳值形式捕獲

  • [&, x]  // x 以值的形式捕獲,其他變量以引用形式捕獲

另外有一點須要注意。對於[=][&]的形式,lambda 表達式能夠直接使用 this 指針。可是,對於[]的形式,若是要使用 this 指針,必須顯式傳入:

C++

1

[this]() { this->someFunc(); }();

至此,咱們已經大體瞭解了 C++ 11 提供的 lambda 表達式的概念。建議經過結合 lambda 表達式與std::sort()std::for_each()這樣的標準函數來嘗試使用一下吧!


  函數對象是C++中以參數形式傳遞函數的一個很好的方法,咱們將函數包裝成類,而且利用()運算符重載實現。

 

?

1
2
3
4
5
6
typedef class hello {
public :
     void operator()( double x) {
         cout << x << endl;
     }
} hello;

  這時候hello是一個類,咱們能夠實例化一個對象hello h;,而後經過h(3.14)的方式來調用這個類的成員函數,若是某個函數須要這個函數做爲回調函數,則能夠將這個hello類的對象傳入便可。

  由於這是一個類的定義,所以咱們徹底能夠在其中定義一些包含額外信息的成員和一些構造函數,讓這個函數對象能夠作更多不一樣的可定製的任務,最終的行爲實際上只是調用了這個()運算符重載函數。這種作法比C++函數指針要容易理解得多,也不容易寫錯。

  而Lambda表達式則是C++中的新語法,實現了許多程序員渴望的部分閉包特性。C++中Lambda表達式能夠被視爲一種匿名函數,這樣,對於一些很是短,並且不太可能被其餘地方的複用的小函數,能夠經過Lambda表達式提升代碼的可讀性。

  在Lambda表達式中對於變量生命期的控制仍是與徹底支持閉包的JavaScript很是不一樣,總而言之,C++對於變量聲明期的控制在新標準中完 全向前兼容,也就是局部變量必定在退出代碼塊時被銷燬,而不是觀察其是否被引用。所以,儘管C++的Lambda表達式中容許引用其代碼上下文中的值,但 是實際上並不可以保證引用的對象必定沒有被銷燬。

  Lambda表達式對於上下文變量的引用有值傳遞和引用傳遞兩種方式,實際上,不管是哪一種方式,在產生Lambda表達式對象時,這些上下文值就已經 從屬於Lambda表達式對象了,也就是說,代碼運行至定義Lambda表達式處時,經過值傳遞方式訪問的上下文變量值已經被寫入Lambda表達式的棧 中,而引用方式傳遞的上下文變量地址被寫入Lambda表達式的棧中。所以,調用Lambda表達式時獲得的上下文變量值就是定義Lambda表達式時這 些變量的值,而引用的上下文變量,若是已經被銷燬,則會出現運行時異常。

  Lambda表達式的基本語法是:

  [上下文變量說明](Lambda表達式參數表) -> 返回類型 { 語句塊 }

  上下文變量說明部分就是說明對於上下文變量的引用方式,=表示值傳遞,&表示引用傳遞,例如,&s就表示s變量採用引用傳遞,不一樣的 說明項之間用逗號分隔,能夠爲空,可是方括號不可以省略。第一項能夠是單獨的一個=或者&,表示,全部上下文變量若無特殊說明一概採用值傳遞/引 用傳遞,什麼都不寫默認爲值傳遞。

  Lambda表達式和TR1標準對應的function<返回類型 (參數表)>對象是能夠互相類型轉換的,這樣,咱們也能夠將Lambda表達式做爲參數進行傳遞,也能夠做爲返回值返回。

  下面看一個Lambda表達式各類使用方法的完整例子:

 

?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// compile with: /EHsc
#include <iostream>
#include <string>
#include <functional> //這是TR1的頭文件,定義了function類模板
using namespace std;
 
typedef class hello {
public :
     void operator()( double x) {
         cout << x << endl;
     }
} hello; //函數對象的定義,也是很是經常使用的回調函數實現方法
 
void callhello(string s, hello func) {
     cout << s;
     func(3.14);
} //一個普通的函數
 
void callhello(string s, const function< void ( double x)>& func) {
     cout << s;
     func(3.14);
} //這個函數會接受一個字符串和一個Lambda表達式做爲參數
 
void callhello(string s, double d) {
     [=] ( double x) {
         cout << s << x << endl;
     }(d);
} //這個函數體內定義了一個Lambda表達式並當即調用
 
function< void ( double x)> returnLambda(string s) {
     cout << s << endl;
     function< void ( double x)> f = ([= /*這裏必須使用值傳遞,由於s變量在returnLambda返回後就被銷燬*/ ] ( double x) {
         cout << s << x << endl;
     });
     s = "changed" ; //這裏對s的修改Lambda表達式是沒法感知的,調用這句語句前s在Lambda表達式中的值已經肯定了
     return f;
} //這個函數接受了一個值傳遞的字符串變量s,咱們將Lambda表達式做爲返回值返回
 
function< void ( double x)> returnLambda2(string& s) {
     cout << s << endl;
     function< void ( double x)> f = ([&s /*這裏可使用引用傳遞,由於s是引用方式傳入的,不隨函數返回而消亡*/ ] ( double x) {
         cout << s << x << endl;
     });
     s = "changed" ; //這裏對s的修改Lambda表達式是能夠感知的,由於s以引用方式參與到Lambda表達式上下文中
     return f;
} //這個函數接受了一個引用傳遞的字符串變量s,將Lambda表達式做爲返回值返回
 
int main()
{
     hello h;
     callhello( "hello:" , h); //用函數對象的方式實現功能
     callhello( "hello lambda:" , -3.14); //這個函數體內定義了一個Lambda表達式並當即調用
     int temp = 6;
     callhello( "hello lambda2:" , [&] ( double x) -> void {
         cout << x << endl;
         cout << temp++ << endl;
     }); //這個函數會接受一個字符串和一個Lambda表達式做爲參數
     cout << temp << endl;
 
     function< void ( double x)> f = returnLambda( "lambda string" ); //這個函數接受了一個值傳遞的字符串變量s,咱們將Lambda表達式做爲返回值返回
     f(3.3);
     string lambdastring2 = "lambda string2" ; //這個變量在main函數返回時才被銷燬
     f = returnLambda2(lambdastring2); //這個函數接受了一個引用傳遞的字符串變量s,將Lambda表達式做爲返回值返回
     f(6.6);
     
     system ( "pause" );
}
相關文章
相關標籤/搜索