C++11初探:lambda表達式和閉包


到了C++11最激動人心的特性了:

匿名函數:lambda表達式

假設你有一個vector<int> v, 想知道里面大於4的數有多少個。for循環誰都會寫,但是STL提供了現成算法count_if,不用可惜。C++03時代,我們會這樣寫:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

bool gt4(int x){
    return x>4;
}

struct GT4{
    bool operator()(int x){
        return x>4;
    }
};

int main(){
    vector<int> v;
   很多v.push_back(...);
   ... cout<<count_if(v.begin(),v.end(),gt4)<<endl; cout<<count_if(v.begin(),v.end(),GT4())<<endl; }

就為這樣一個微功能,要么寫一個函數,要么寫個仿函數functor,還不如手寫循環簡單,這是我的感受。如果用過其他語言的lambda表達式,這種寫法完全是渣渣。

C++引入的lambda表達式提供了一種臨時定義匿名函數的方法,可以這樣寫:

int res = count_if(v.begin(),v.end(),[](int x){ return x>4; });

世界瞬間美好了。既然是匿名函數,函數名自然不用寫了,連返回類型都不用寫了~想用一個函數,用的時候再寫,大大提高了algorithm里各種泛型算法的實用性。

一般的lambda表達式語法是

[捕獲列表] (參數列表) -> 返回類型 {函數體}

->返回類型可以省略;如果是無參的,(參數列表)也可以省略,真是各種省。匿名函數是個lambda對象,和函數指針有區別,但一般不用關心它。如果你想把一個匿名函數賦給一個函數指針類似物以待后續使用,可以用auto

auto func = [](int arg) { ... };

但捕獲列表是什么?接下來:

閉包closure

如果改主意了,要求>k的個數,k運行時指定,怎么辦?你可能會寫

int k;
cin>>k;
int res = count_if(v.begin(),v.end(),[](int x){
    return x>k;
}); //WRONG!

但是編譯器報錯:

error: variable 'k' cannot be implicitly captured in a lambda
      with no capture-default specified
        return x>k;

匿名函數不能訪問外部函數作用域的變量?太弱了!

如果真是這樣,實用性的確有限。lambda的捕獲列表就是指定你要訪問哪些外部變量,這里是k,於是

int res = count_if(v.begin(),v.end(),[k](int x){ //注意[]里的
     return x>k;
}); //OK!

如果要捕獲多個變量,可以用逗號隔開。如果要捕獲很多變量,干脆一起打包算了,用'='捕獲所有:

int res = count_if(v.begin(),v.end(),[=](int x){
     return x>k;
}); //OK, too!

通俗的說:子函數可以使用父函數中的局部變量,這種行為就叫做閉包。

解釋一下各種捕獲方式:

捕獲capture有些類似傳參。使用[k], [=]聲明的捕獲方式是復制copy,類似傳值。區別在於,函數參數傳值時,對參數的修改不影響外部變量,但copy的捕獲直接禁止你去修改。如果想修改,可以使用引用方式捕獲,語法是[&k]或[&]。引用和復制可以混用,如

int i,j;
[=i, &j] (){...};

 

但閉包的能力遠不止“使用外部變量”這么簡單,最奇幻的是它可以超越傳統C++對變量作用域生存期的限制。我們嘗試一些刺激的。

假設你要寫一個等差數列生成器,初值公差運行時指定,行為和函數類似,第k次調用生成第k個值,並且各個生成器互不干擾,怎么寫?

普通函數不好優雅地保存狀態(全局變量無力了吧)。用仿函數好了,成員變量保存每個計數器的狀態:

struct Counter
{
    int cur;
    int step;
    Counter(int _init,int _step){
        cur = _init;
        step = _step;
    }
    int operator()(){
        cur = cur+step;
        return cur-step;
    }
};
int main(){
    Counter c(1,3);
    for(int i=0;i<4;++i){
        cout<<c()<<endl;
    } //輸出1 4 7 10 
}

但是我們現在有了閉包!把狀態作為父函數中的局部變量,各個counter就可以不影響了。由於要修改外部變量,根據之前的介紹,聲明成引用捕獲[&]。寫起來大體像這樣:

??? Counter(int init,int step){
    int cur = init;
    return [&]{
        cur += step;
        return cur-step;
    }
}
int main(){
    auto c = Counter(1,3);
    for(int i=0;i<4;++i){
        cout<<c()<<endl;
    }
}

兩個問題!

第一個:Counter函數的返回類型怎么寫???

Counter返回值是一個lambda,賦給c時可以用auto騙過去,但聲明時寫類型是躲不過去了。返回類型后置+decltype救不了你,因為后置了decltype還是獲取不到返回值類型。lambda對象,雖然行為像函數指針,但是不能直接賦給一個函數指針。

介紹一個C++11新的模板類function,是消滅丑陋函數指針的大殺器。你可以把一個函數指針或lambda賦給它,例如

 
         
#include <functional>
int func(float a,float b) {
return a+b;
} function
<int(float,float)> pfunc = func; function<int(float,float)> plambda = [](float a,float b){ return a+b;};

比函數指針好看多了。

於是這里可以寫:

function<int()> Counter(int init,int step){ ... }

但是!如果再瘋狂一點,匿名函數可以省略返回類型,auto可以推導類型,結合起來這樣寫是可以的!

auto Counter = [](int init,int step){
    int cur = init;
    return [&](){
        cur += step;
        return cur-step;
    };
}; //不要漏';' 根本上還是賦值語句

“類型推導, auto和decltype”一節里留的trick就是這個。javascript的即視感有木有!

第二個:編譯通過,運行輸出是這個???

1
167772160
167772160
167772160

看起來像是訪問了無效內存。的確是這樣。cur,step這兩個局部變量在父函數的棧幀中,內部的匿名函數返回以后,父函數的棧幀就銷毀了,而我們用的是“引用”,引用的變量已經沒了。

既然放在棧上會有生存期問題,那就放堆里

auto Counter = [](int init,int step){
    int* pcur = new int(init);
    int* pstep = new int(step);
    return [=](){ //注意!&變成了=
        *pcur += *pstep;
        return *pcur-*pstep;
    };
};

注意使用了[=]而不是[&]。解釋:

  1. 我們沒有直接修改捕獲的指針變量,而是修改它指向的變量,和[=]的規則不沖突
  2. 外部的指針還是在棧上,如果用[&]還是會引用到已銷毀的指針。我們只需要復制一份指針值。

這樣輸出的確正常了,但是內存泄漏了。程序員的節操呢?

用智能指針可以解決內存泄漏:

auto Counter = [](int init,int step){
    shared_ptr<int> pcur(new int(init));
    shared_ptr<int> pstep(new int(step));
    return [=](){
        *pcur += *pstep;
        return *pcur-*pstep;
    };
};

雖然解決了問題,但過於繁瑣了。本質上,我們需要的效果是把父函數的局部變量生存期延長,至少和子函數一樣長。C++11提供了mutable關鍵字,可以模擬這一功能:

auto Counter = [](int init,int step){
    int cur = init;
    return [=] () mutable {
        cur += step;
        return cur-step;
    };
};

加上mutable,就告訴編譯器,這個變量是父子函數共享的,子函數對它的修改要反映到外部,並且它的生存期要和子函數一樣長!

這里可能有點繞,函數哪來的生存期?注意這里“子函數”並不是真正的函數,只是一個lambda類型的變量,只是有函數的行為,一樣有生存期。

 閉包最大的用處在於寫回調函數,比如事件響應。當初學Java的時候,Swing里用戶界面各種內部類,感覺很煩。現在Java終於也有閉包了(Java8)~

目錄


免責聲明!

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



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