lambda表達式


lambda 表達式是 C++11 最重要也最常用的一個特性之一,C# 3.5 和 Java 8 中就引入了 lambda 表達式。


lambda 來源於函數式編程的概念,也是現代編程語言的一個特點。C++11 這次終於把 lambda 加進來了。

lambda表達式有如下優點:

聲明式編程風格:就地匿名定義目標函數或函數對象,不需要額外寫一個命名函數或者函數對象。以更直接的方式去寫程序,好的可讀性和可維護性。

簡潔:不需要額外再寫一個函數或者函數對象,避免了代碼膨脹和功能分散,讓開發者更加集中精力在手邊的問題,同時也獲取了更高的生產率。

在需要的時間和地點實現功能閉包,使程序更靈活。下面,先從 lambda 表達式的基本功能開始介紹它。

lambda 表達式的概念和基本用法

lambda 表達式定義了一個匿名函數,並且可以捕獲一定范圍內的變量。lambda 表達式的語法形式可簡單歸納如下:

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

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

因此,一個完整的 lambda 表達式看起來像這樣:

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

可以看到,上面通過一行代碼定義了一個小小的功能閉包,用來將輸入加 1 並返回。
在 C++11 中,lambda 表達式的返回值是通過前面介紹的《C++返回值類型后置》語法來定義的。其實很多時候,lambda 表達式的返回值是非常明顯的,比如這個例子。因此,C++11 中允許省略 lambda 表達式的返回值定義:

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

這樣編譯器就會根據 return 語句自動推導出返回值類型。
需要注意的是,初始化列表不能用於返回值的自動推導:

auto x1 = [](int i){ return i; };  // OK: return type is int
auto x2 = [](){ return { 1, 2 }; };  // error: 無法推導出返回值類型

不能對參數進行直接賦值,且可以有多個參數。這時我們需要顯式給出具體的返回值類型。

另外,lambda 表達式在沒有參數列表時,參數列表是可以省略的。因此像下面的寫法都是正確的:

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

  

使用 lambda 表達式捕獲列表

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

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


下面看一下它的具體用法,如下所示。

【實例】lambda 表達式的基本用法。

class A
{
    public:
    int i_ = 0;
    void func(int x, int y)
    {
        auto x1 = []{ return i_; };                    // error,沒有捕獲外部變量
        auto x2 = [=]{ return i_ + x + y; };           // OK,捕獲所有外部變量
        auto x3 = [&]{ return i_ + x + y; };           // OK,捕獲所有外部變量
        auto x4 = [this]{ return i_; };                // OK,捕獲this指針
        auto x5 = [this]{ return i_ + x + y; };        // error,沒有捕獲x、y
        auto x6 = [this, x, y]{ return i_ + x + y; };  // OK,捕獲this指針、x、y
        auto x7 = [this]{ return i_++; };              // OK,捕獲this指針,並修改成員的值
    }
};
int a = 0, b = 1;
auto f1 = []{ return a; };               // error,沒有捕獲外部變量
auto f2 = [&]{ return a++; };            // OK,捕獲所有外部變量,並對a執行自加運算
auto f3 = [=]{ return a; };              // OK,捕獲所有外部變量,並返回a
auto f4 = [=]{ return a++; };            // error,a是以復制方式捕獲的,無法修改
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 表達式無法修改通過復制方式捕獲的外部變量。如果希望修改這些變量的話,我們需要使用引用方式進行捕獲。

一個容易出錯的細節是關於 lambda 表達式的延遲調用的:

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

在這個例子中,lambda 表達式按值捕獲了所有外部變量。在捕獲的一瞬間,a 的值就已經被復制到f中了。之后 a 被修改,但此時 f 中存儲的 a 仍然還是捕獲時的值,因此,最終輸出結果是 0。

如果希望 lambda 表達式在調用時能夠即時訪問外部變量,我們應當使用引用方式捕獲。

從上面的例子中我們知道,按值捕獲得到的外部變量值是在 lambda 表達式定義時的值。此時所有外部變量均被復制了一份存儲在 lambda 表達式變量中。此時雖然修改 lambda 表達式中的這些外部變量並不會真正影響到外部,我們卻仍然無法修改它們。

那么如果希望去修改按值捕獲的外部變量應當怎么辦呢?這時,需要顯式指明 lambda 表達式為 mutable

int a = 0;
auto f1 = [=]{ return a++; };             // error,修改按值捕獲的外部變量
auto f2 = [=]() mutable { return a++; };  // OK,mutable

 被 mutable 修飾的 lambda 表達式就算沒有參數也要寫明參數列表。

lambda 表達式的類型

最后,介紹一下 lambda 表達式的類型。

lambda 表達式的類型在 C++11 中被稱為“閉包類型(Closure Type)”。它是一個特殊的,匿名的非 nunion 的類類型。

因此,我們可以認為它是一個帶有 operator() 的類,即仿函數。因此,我們可以使用 std::function 和 std::bind 來存儲和操作 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 表達式則不存在這個問題。
這里也可以很自然地解釋為何按值捕獲無法修改捕獲的外部變量。因為按照 C++ 標准,lambda 表達式的 operator() 默認是 const 的。一個 const 成員函數是無法修改成員變量的值的。而 mutable 的作用,就在於取消 operator() 的 const。
需要注意的是,沒有捕獲變量的 lambda 表達式可以直接轉換為函數指針,而捕獲變量的 lambda 表達式則不能轉換為函數指針。看看下面的代碼:

typedef void(*Ptr)(int*);
Ptr p = [](int* p){delete p;};  // 正確,沒有狀態的lambda(沒有捕獲)的lambda表達式可以直接轉換為函數指針
Ptr p1 = [&](int* p){delete p;};  // 錯誤,有狀態的lambda不能直接轉換為函數指針

上面第二行代碼能編譯通過,而第三行代碼不能編譯通過,因為第三行的代碼捕獲了變量,不能直接轉換為函數指針。

 原文地址:http://c.biancheng.net/view/3741.html

 


免責聲明!

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



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