1. 程式人生 > >C11簡潔之道:lambda表達式

C11簡潔之道:lambda表達式

編譯 per bool code 操作 定義函數 ica 自加運算 返回

1、 定義

  lambda表達式是C++11非常重要也是很常用的特性之一,來源於函數式編程的概念,也是現代編程語言的一個特點。它有如下特點:

  • 聲明式編程風格:就地匿名定義目標函數或者函數,不需要額外寫一個命名函數或者函數對象,以更直接的方式寫程序。
  • 簡潔:不需要額外再寫一個函數或者函數對象,避免了代碼膨脹和功能分散。
  • 在需要的時間和地點實現功能閉包,使程序更加靈活。

  lambda表達式定義一個匿名函數,並且可以捕獲一定範圍內的變量,其定義如下:

  [captrue] (params) opt -> ret {body};

  其中,capture是捕獲列表;params是參數列表;opt是函數選項;ret是返回值類型;body是函數體。

  我們來做一個簡單的例子,定義一個簡單的封包,用來將輸入的結果+1並返回。

auto f = [](int a) -> int {return a + 1; };
std::cout << f(1) << std::endl;

  lambda表達式的返回值通過返回值後置語法來定義,所以很多時候可以省略返回值類型,編譯器根據return語句自動推導返回值類型。

auto f = [] (int a) {return a + 1;};

  但是初始化列表不能作為返回值的自動推導,需要顯示給出具體的返回值類型。

auto f1 = [] (int a) {return
a + 1;}; //ok,return type is int auto f2 = [] () {return {1, 2}};    //error:無法推導出返回值類型

  lambda表達式在沒有參數列表的時候,參數列表可以省略。

auto f1 = [] () {return 1;};
auto f2 = [] {return 1;} //省略空參數表

2、 捕捉

  lambda表達式可以通過捕獲列表捕獲一定範圍內的變量:

  • []不捕獲任何變量;
  • [&]捕獲外部作用域所有變量,並作為引用在函數體使用(按引用捕獲);
  • [=]捕獲外部作用域作用變量,並作為副本在函數體使用(按值捕獲);
  • [=,&foo]按值捕獲外部作用域所有變量,並按引用捕獲foo變量;
  • [bar]按值捕獲bar變量,同時不捕獲其他變量;
  • [this]捕獲當前類中的this指針,讓lambda擁有和當前類成員函數同樣的訪問權限,如果已經使用了&或者=,就默認添加此選項。捕獲this的目的是可以在lambda中使用當前類的成員函數和成員變量。
class A
{
public:
    int mi = 0;

    void func(int x, int y)
    {
        auto x1 = []{return mi;};                      //error,沒有捕獲外部變量
        auto x2 = [=] {return mi + x + y;};            //ok,按值捕獲所有外部變量
        auto x3 = [&] {return mi + x + y;};            //ok,按引用捕獲所有外部變量
        auto x4 = [this] {return mi;};                 //ok,捕獲this指針
        auto x5 = [this] {return mi + x + y;};         //error,沒有捕獲x,y
        auto x6 = [this,x,y] {return mi + x + y;};     //ok,捕獲this,x,y
        auto x7 = [this] {return mi++;};               //ok,捕獲this指針,並修改成員的值
    }
};

int a = 0, b = 2;
auto f1 = [] {return a;} ;                 //error,沒有捕獲外部變量
auto f2 = [&] {return a++;};               //ok,按引用捕獲所有外部變量,並對a執行自加運算
auto f3 = [=] {return a;};                 //ok,按值捕獲所有外部變量,並返回a
auto f4 = [=] {return a++;};               //error,按值引用不能改變值
auto f5 = [a] {return a + b;};             //error,沒有捕獲b
auto f6 = [a, &b] {return a + (b++);};     //ok,捕獲a和b的值,並對b做自加運算
auto f7 = [=, &b] {return a + (b++);};     //ok,捕獲所有外部變量和b的引用,並對b做自加運算

  從例子中可以看到,lambda表達式的捕獲列表精細的控制表達式能夠訪問的外部變量,以及如何訪問這些變量。需要註意的是,默認狀態下的lambda表達式無法修改通過復制方式捕獲的外部變量,如果希望修改這些變量,需要用引用的方式進行修改。但是按值引用的話,如果延遲調用,那麽在調用該lambda表達式的時候,其捕獲的變量值還是在lambda定義的時候的值。

int a = 0;
auto f = [=] {return a;};      //按值捕獲外部變量
a += 1;                        //修改值
std::cout << f() << std::endl; //輸出0            

  這個例子中,lambda表達式按值捕獲了所有外部變量,在捕獲的一瞬間a的值就已經被賦值在其中,之後a值修改並不會改變lambda表達中存的a的值,因此最終結果輸出還是0。如果希望lambda表達式在調用的時候能即時訪問外部變量,應當使用引用的方式捕獲。

  如果希望去修改按值捕獲的外部變量,需要顯示指明lambda表達式為mutable,但是被mutable修飾的lambda表達式有一個特點,就是無論有沒有參數都要寫明參數列表。

int a = 0;
auto f1 = [=] {return a++;}; //error,不能修改按值捕獲的變量
auto f2 = [=] mutable {return a++; }; //ok,mutable

  lambda表達式其實是一個帶有operator()的類,即仿函數,因此我們可以使用std::bind和std::function來存儲和操作lambda表達式:

std::function<int(int)> f1 = [] (int a){ return a;};
std::function<int(void)> f2 = std::bind([](int a) {return a}, 123);

  對於沒有捕獲任何變量的lambda表達式,還可以被轉換成一個普通的函數指針:

using func_t = int(*)(int);
func_t f = [](int a){return a;};
f(123);

  lambda表達式可以說是定義仿函數閉包的語法糖,它捕獲的任何外部變量都會轉換為閉包類型的成員變量。而使用成員變量的類的operator(),如果能直接轉換為普通的函數指針,那lambda表達式本身的this指針會丟失,沒有捕獲任何外部變量的lambda表達式則不存在這個問題,所以按值捕獲的外部變量無法修改。因為lambda表達式中的operator()默認是const的,一個const成員函數無法修改成員變量的值,而mutable則是取消operator()的const。

  所以,沒有捕獲變量的lambda表達式可以直接轉換為函數指針,而捕獲變量的lambda表達式則不能轉換為函數指針。

typedef void(*Ptr)(int *);
Ptr p1 = [](int *p) {delete p;}; //ok,沒有捕獲的lambda表達式可以轉換為函數指針
Ptr p2 = [&](int *p){delete p;}; //error,有捕獲的lambda表達式不能直接轉換為函數指針,不能通過編譯

3、 簡潔代碼

  就地定義匿名函數,不再需要定義函數對象,大大簡化了標準庫的調用。比如我們使用for_each將vector中的偶數打印出來。

class CountEven
{
private:
    int &micount;

public:
    CountEven(int &count) : micount(count) {}

    void operator()(int val)
    {
        if(!(val & 1))
        {
            ++micount;
        }
    }       
};

std::vector<int> v = {1, 2, 3, 4, 5, 6};
int count = 0;
for_each(v.begin(), v.end(), CountEven(count));
std::cout << "The number:" << count << std::endl;

  這樣寫比較繁瑣,如果用lambda表達式,使用閉包概念來替換這裏的仿函數。

std::vector<int> v = {1, 2, 3, 4, 5, 6};
int count = 0;

for_each(v.begin(), v.end(), [&count] (int val)
{
    if(!(val & 1))
    {
        ++count;
    }
});

  在之前的例子中,使用std::bind組合多個函數,實現了計算集合中大於5小於10的元素個數。

using std::placeholders::_1;
auto f = std::bind(std::logical_and<bool>(),
std::bind(std::greater<int>(), std::placeholders::_1, 5),
std::bind(std::less_equal<int>(), std::placeholders::_1, 10));
int count = std::count_if(coll.begin(), coll.end(), f);

  通過lambda表達式可以輕松的實現類似的功能:

  //查找大於5小於10的元素個數

int count  = std::count_if(coll.begin(), coll.end(), [](int x) {return x > 5 && x < 10;})

  lambda表達式比std::bind更加靈活和簡潔,如果簡單的邏輯處理,用lambda表達式來代替function,提升開發效率,效果會更好。

C11簡潔之道:lambda表達式