c++中lambda表達式用法

說明一下,我用的是gcc7.1.0編譯器,標準庫源代碼也是這個版本的。

本篇文章講解c++11中lambda表達式用法。python

初次接觸lambda這個關鍵字,記得仍是在python裏面,但其實,早在2011年c++11推出來的時候咱們c++就有了這個關鍵字啦。lambda表達式是C++11中引入的一項新技術,利用lambda表達式能夠編寫內嵌的匿名函數,用以替換獨立函數或者函數對象,而且使代碼更可讀。ios

所謂函數對象,其實就是對operator()進行重載進而產生的一種行爲,好比,咱們能夠在類中,重載函數調用運算符(),此時類對象就能夠直接相似函數同樣,直接使用()來傳遞參數,這種行爲就叫作函數對象,一樣的,它也叫作仿函數。c++

若是從廣義上說,lambda表達式產生的是也是一種函數對象,由於它也是直接使用()來傳遞參數進行調用的。算法

1 lambda表達式基本使用

lambda表達式基本語法以下:shell

[ 捕獲 ] ( 形參 ) -> ret { 函數體 };

lambda表達式通常都是以方括號[]開頭,有參數就使用(),無參就直接省略()便可,最後結束於{},其中的ret表示返回類型。閉包

咱們先看一個簡單的例子,定義一個能夠輸出字符串的lambda表達式,完整的代碼以下:函數

#include <iostream>

int main()
{
    auto atLambda = [] {std::cout << "hello world" << std::endl;};
    atLambda();
    return 0;
}

上面定義了一個最簡單的lambda表達式,沒有參數。若是須要參數,那麼就要像函數那樣,放在圓括號裏面,若是有返回值,返回類型則要放在->後面,也就是尾隨返回類型,固然你也能夠忽略返回類型,lambda會幫你自動推導出返回類型,下面看一個較爲複雜的例子:this

#include <iostream>

int main()
{
    auto print = [](int s) {std::cout << "value is " << s << std::endl;};
    auto lambAdd = [](int a, int b) ->int { return a + b;};
    int iSum = lambAdd(10, 11);
    print(iSum);

    return 0;
}

lambAdd有兩個入參a和b,而後它的返回類型是int,咱們能夠試一下把->int去掉,結果是同樣的。spa

2 lambda捕獲塊
2.1 捕獲的簡單使用

在第1節中,咱們展現了lambda的語法形式,後面的形參和函數體之類都好理解,那麼方括號裏面捕獲是啥意思呢?指針

其實這裏涉及到lambda表達式一個重要的概念,就是閉包。

這裏咱們須要先對lambda表達式的實現原理作一下說明:當咱們定義一個lambda表達式後,編譯器會自動生成一個匿名類,這個類裏面會默認實現一個public類型的operator()函數,咱們稱爲閉包類型。那麼在運行時,這個lambda表達式就會返回一個匿名的閉包實例,它是一個右值。

因此,咱們上面的lambda表達式的結果就是一個一個的閉包。閉包的一個強大之處是能夠經過傳值或者引用的方式捕獲其封裝做用域內的變量,前面的方括號就是用來定義捕獲模式以及變量,因此咱們把方括號[]括起來的部分稱爲捕獲塊。

看這個例子:

#include <iostream>

int main()
{
    int x = 10;
    auto print = [](int s) {std::cout << "value is " << s << std::endl;};
    auto lambAdd = [x](int a) { return a + x;};
    auto lambAdd2 = [&x](int a, int b) { return a + b + x;};
    auto iSum = lambAdd(10);
    auto iSum2 = lambAdd2(10, 11);
    print(iSum);
    print(iSum2);

    return 0;
}

當lambda塊爲空時,表示沒有捕獲任何變量,不爲空時,好比上面的lambAdd是以複製的形式捕獲變量x,而lambAdd2是以引用的方式捕獲x。那麼這個複製或者引用究竟是怎麼體現的呢,咱們使用gdb看一下lambAdd和lambAdd2的具體類型,以下:

(gdb) ptype lambAdd
type = struct <lambda(int)> {
    int __x;
}
(gdb) ptype lambAdd2
type = struct <lambda(int, int)> {
    int &__x;
}
(gdb)

前面咱們說過lambda其實是一個類,這裏獲得了證實,在c++中struct和class除了有少量區別,其餘都是同樣的,因此咱們能夠看到複製形式捕獲其實是一個包含int類型成員變量的struct,引用形式捕獲其實是一個包含int&類型成員變量的struct,而後在運行的時候,會使用咱們捕獲的數據來初始化成員變量。

既然有初始化,那麼必然有構造函數啊,而後捕獲生成的成員變量,有operator()函數,暫時來說,一個比較立體的閉包類型就存在於咱們腦海中啦,對於lambda表達式類型具體組成,咱們暫時放一放,接着說捕獲。

2.2 捕獲的類型

捕獲的方式能夠是引用也能夠是複製,可是到底有哪些類型的捕獲呢?

捕獲類型以下:

  • []:默認不捕獲任何變量;
  • [=]:默認以複製捕獲全部變量;
  • [&]:默認以引用捕獲全部變量;
  • [x]:僅以複製捕獲x,其它變量不捕獲;
  • [x...]:以包展開方式複製捕獲參數包變量;
  • [&x]:僅以引用捕獲x,其它變量不捕獲;
  • [&x...]:以包展開方式引用捕獲參數包變量;
  • [=, &x]:默認以複製捕獲全部變量,可是x是例外,經過引用捕獲;
  • [&, x]:默認以引用捕獲全部變量,可是x是例外,經過複製捕獲;
  • [this]:經過引用捕獲當前對象(實際上是複製指針);
  • [*this]:經過複製方式捕獲當前對象;

能夠看到,lambda是能夠有多個捕獲的,每一個捕獲之間以逗號分隔,另外呢,無論多少種捕獲類型,萬變不離其宗,要麼以複製方式捕獲,要麼以引用方式捕獲。

那麼複製捕獲和引用捕獲到底有什麼區別呢?

標準c++規定,默認狀況下,在lambda表達式中,對於operator()的重載是const屬性的,也就意味着若是以複製形式捕獲的變量,是不容許修改的,看這段代碼:

#include <iostream>

int main()
{
    int x = 10;
    int y = 20;
    auto print = [](int s) {std::cout << "value is " << s << std::endl;};
    auto lambAdd = [x](int a) { 
    //    x++;  此處x是隻讀,不容許自增,編譯會報錯
        return a + x;
    };
    auto lambAdd2 = [&x](int a, int b) { 
        x = x+5;
        return a + b + x;
    };
    auto iSum = lambAdd(10);
    auto iSum2 = lambAdd2(10, 11);
    print(iSum);
    print(iSum2);

    return 0;
}

從代碼能夠看出,複製捕獲不容許修改變量值,而引用捕獲則容許修改變量值,爲何呢,這裏我理解,&x其實是一個int*類型的指針,因此咱們能夠修改x的值,由於咱們只是對這個指針所指向的內容進行修改,並無對指針自己進行修改,且與咱們常規聲明的引用類型入參同樣,修改的值在lambda表達式外也是有效的。

那麼若是我想使用複製捕獲,又想修改變量的值呢,這時咱們就想起來有個關鍵字,叫作mutable,它容許在常成員函數中修改爲員變量的值,因此咱們能夠給lambda表達式指定mutable關鍵字,以下:

#include <iostream>

int main()
{
    int x = 10;
    int y = 20;
    auto print = [](int s) {std::cout << "value is " << s << std::endl;};
    auto lambAdd = [x](int a) mutable { 
        x++;
        return a + x;
    };
    auto iSum = lambAdd(10);
    print(iSum);
    print(x);

    return 0;
}

執行結果以下:

value is 21
value is 10

因此加上mutable之後就能夠對複製捕獲進行修改,但有一點,它的修改出了lambda表達式之後就無效了。

2.3 包展開方式捕獲

仔細看2.2節中捕獲類型,會發現有[x...]這樣的類型,它其實是以複製方式捕獲了一個可變參數,在c++中其實涉及到了模板形參包,也就是變參模板,看下面例子:

#include <iostream>

void tprintf()
{
    return;
}

template<typename U, typename ...Ts>
void tprintf(U u, Ts... ts)
{
    auto t = [ts...]{
        tprintf(ts...);
    };
    std::cout << "value is " << u << std::endl;
    t();
    return;
}

int main()
{
    tprintf(1,'c',3, 8);
    return 0;
}

它捕獲了一組可變的參數,不過這裏其實是爲了演示對可變參數的捕獲,強行使用了lambda表達式,不使用的話,代碼可能更加簡潔,咱們只須要經過這個演示知道怎麼使用便可,另外對於變參模板的使用,這裏就不展開來說了。

2.4 捕獲的做用

我再看lambda的捕獲的時候一直很奇怪,初看的話,這個捕獲跟傳參數有什麼區別呢,都是把一個變量值傳入lambda表達式體供使用,但仔細思考的話,它是有做用的,假設有這麼一個案例,一個公司有999名員工,每一個員工的工號是從1~999,咱們如今想找出工號是8的整數倍的全部員工,一個可行的代碼以下:

#include <iostream>
#include <array>

int main()
{
    int x = 8;
    auto t = [x](int i){
        if ( i % x == 0 )
        {
            std::cout << "value is " << i << std::endl;
        }
    };
    auto t2 = [](int i, int x){
        if ( i % x == 0 )
        {
            std::cout << "value is " << i << std::endl;
        }
    };
    for(int j = 1; j< 1000; j++)
    {
        t(j);
        t2(j, x);
    }
    return 0;
}

表達式t使用了捕獲,而表達式t2沒有使用捕獲,從代碼做用和量來看,它們其實區別不大,但有一點,對於表達式t,x的值只複製了一次,而對於t2表達式,每次調用都要生成一個臨時變量來存放x的值,這實際上是多了時間和空間的開銷,不過,對於這段代碼而言,這點消耗能夠忽略不計呢,但一旦數據上了規模,那就會有比較大的區別了。

對於捕獲的做用,我暫時只想到了這一點,若是有大佬知道更多的做用,麻煩說一下呀。

對於捕獲,仍是儘可能不要使用[=]或者[&]這樣全捕獲的形式,由於不可控,你不能確保哪些變量會被捕獲,容易發生一些不測的行爲。

3 lambda表達式做爲回調函數

lambda表達式一個更重要的應用是它能夠做爲函數的參數傳入,經過這種方式能夠實現回調函數。好比在STL算法中,常常要給一些模板類或者模板函數來指定某個模板參數爲lambda表達式,就想上一節說的,我想統計999個員工中工號是8的整數倍的員工個數,一個可用的代碼以下:

#include <iostream>
#include <array>
#include <algorithm>

int main()
{
    int x = 8;
    std::array<int, 999> arr;
    for (int i =1; i< 1000; i++)
    {
        arr[i] = i;
    }
    int cnt = std::count_if(arr.begin(), arr.end(), [x](int a){ return a%x == 0;});
    std::cout << "cnt=" << cnt << std::endl;
    return 0;
}

這裏很明顯,咱們指定了一個lambda表達式來做爲一個條件,更多時候,是使用排序函數的時候,指定排序準則,也可使用lambda表達式。

4 lambda表達式賦值

lambda表達式既然生成了一個類對象,那麼它是否能夠像普通類對象那樣,進行賦值呢?

咱們寫一段代碼試一下:

#include <iostream>
using namespace std;

int main()
{
    auto a = [] { cout << "A" << endl; };
    auto b = [] { cout << "B" << endl; };
 
    //a = b; // 非法,lambda沒法賦值
    auto c(a); // 合法,生成一個副本
    return 0;
}

很顯然賦值不能夠,而拷貝則能夠,結合編譯器自動生成構造函數規則,很明顯,賦值函數被禁用了,而拷貝構造函數則沒有被禁用,因此不能用一個lambda表達式給另一個賦值,但能夠進行初始化拷貝。

5 總結

總而言之,根據lambda表達式的一個定義來看,它實際上是用於替代一些功能比較簡單,但又有大量使用的函數,lambda在stl中大量使用,對於大部分STL算法而言,能夠很是靈活地搭配lambda表達式來實現想要的效果。

同時這裏要說明一下,lambda實際上是做爲c++11新引入的一種語法規則,它與STL並無什麼直接關聯,只是STL裏面大量使用了lambda表達式而已,並不能直接就說把它當作是STL的一部分。

相關文章
相關標籤/搜索