1. 程式人生 > >C++ lambda表示式總結

C++ lambda表示式總結

    一個lambda表示式用於建立閉包。lambda表示式與任何函式類似,具有返回型別、引數列表和函式體。與函式不同的是,lambda能定義在函式內部。lambda表示式具有如下形式

[ capture list ] ( parameter list) -> return type { function body }

 capture list,捕獲列表,是一個lambda所在函式中定義的區域性變數的列表。lambda函式體中可以使用這些區域性變數。捕獲可以分為按值捕獲和按引用捕獲。非區域性變數,如靜態變數、全域性變數等可以不經捕獲,直接使用;

parameter list,引數列表。從C++14開始,支援預設引數,並且引數列表中如果使用auto的話,該lambda稱為泛化lambda(generic lambda);

return type,返回型別,這裡使用了返回值型別尾序語法(trailing return type synax)。可以省略,這種情況下根據lambda函式體中的return語句推斷出返回型別,就像普通函式使用decltype(auto)推導返回值型別一樣;如果函式體中沒有return,則返回型別為void。

function body,與任何普通函式一樣,表示函式體。

 

Lambda表示式可以忽略引數列表和返回型別,但必須包含捕獲列表和函式體:

auto f = [] { return 42; }
cout << f() << endl;

          上面的lambda表示式,定義了一個可呼叫物件f,它不接受引數,返回42。Lambda的呼叫方式與普通函式的呼叫方式相同。

        

         lambda表示式是用於生成閉包的純右值(prvalue)表示式。每一個lambda表示式都定義了獨一無二的閉包類,閉包類內主要的成員有operator()成員函式:

ret operator()(params) const { body } //the keyword mutable was not used
ret operator()(params) { body } //the keyword mutable was used

template<template-params> //since C++14, generic lambda
ret operator()(params) const { body }

template<template-params> //since C++14, generic lambda, the keyword mutable was used
ret operator()(params) { body }

          當呼叫lambda表示式生成的閉包時,執行operator()函式。除非lambda表示式中使用了mutable關鍵字,否則lambda生成的閉包類的operator()函式具有const飾詞,從而lambda函式體中不能修改其按值捕獲的變數;如果lambda表示式的引數列表中使用了auto,則相應的引數稱為模板成員函式operator()的模板形參,該lambda表示式也就成了泛化lambda表示式。

         如果捕獲列表中,有按值捕獲的區域性變數,則閉包類中就會有相應的未命名成員變數副本,這些成員變數在定義lambda表示式時就由那些相應的區域性變數進行初始化。如果按值捕獲的變數是個函式引用,則相應的成員變數是引用指向函式的左值引用;如果是個物件引用,則相應的成員變數是該引用指向的物件。如果是按引用捕獲,標準中未指明是否會在閉包類中引入相應的成員變數。

         該閉包類還有其他成員函式。比如轉換為函式指標的轉換函式、建構函式(包括複製建構函式)、解構函式等,具體可參考https://en.cppreference.com/w/cpp/language/lambda

 

一:捕獲列表

lambda可以定義在函式內部,使用其區域性變數,但它只能使用那些明確指明的變數。lambda通過將外部函式的區域性變數包含在其捕獲列表中來指出將會使用這些變數。

當定義一個lambda時,編譯器生成一個與lambda對應的新的(未命名的)類。當向函式傳遞一個lambda時,同時定義了一個新型別和該型別的一個物件:傳遞的引數就是編譯器生成的類型別的未命名物件;類似的,當使用auto定義一個用lambda初始化的變數時,定義了一個從lambda生成的型別的物件。

    預設情況下,從lambda生成的類都包含一個對應該lambda所捕獲的變數的資料成員。類似任何普通類的資料成員,lambda的資料成員在lambda物件建立時被初始化。

1:值捕獲

類似引數傳遞,變數的捕獲方式可以是值或引用。與傳值引數類似,採用值捕獲的前提是變數可以拷貝。被捕獲的變數的值是在lambda建立時拷貝,而不是呼叫時拷貝:

int v1 = 42;
auto f=[v1]{return v1;};
v1=0;
auto j = f(); //j is 42

 由於被捕獲變數的值是在lambda建立時拷貝,因此隨後對其修改不會影響到lambda內對應的值。

2:引用捕獲

定義lambda時可以採用引用方式捕獲變數。例如:

int v1 = 42;
auto f=[&v1]{return v1;};
v1=0;
auto j = f(); //j is 0

 v1之前的&指出v1應該以引用方式捕獲。一個以引用方式捕獲的變數與其他任何型別的引用的行為類似。當我們在lambda函式體內使用此變數時,實際上使用的是引用所繫結的物件。在本例中,當lambda返回v1時,它返回的是v1指向的物件的值。

引用捕獲與返回引用有著相同的問題和限制。如果我們採用引用方式捕獲一個變數,就必須確保被引用的物件在lambda執行的時候是存在的。lambda捕獲的都是區域性變數,這些變數在函式結束後就不復存在了。如果lambda可能在函式結束後執行,捕獲的引用指向的區域性變數已經消失,這就是未定義行為。

    引用捕獲有時是必要的:

void biggies(vector<string> &words,
             vector<string>::size_ type sz,
             ostream &os=cout, char c=' ')
{
    for_each(words.begin(), words.end(), 
              [&os, c](const strinq &s) { os << s << c; });
}

 不能拷貝ostream物件,因此捕獲os的唯一方法就是捕獲其引用。當我們向一個函式傳遞lambda時,就像本例子呼叫for_each那樣,lambda會在函式內部執行。在此情況下,以引用方式捕獲os沒有問題,因為當for_each執行時,biggies中的變數是存在的。

我們也可以從一個函式返回lambda。函式可以直接返問一個可呼叫物件,或者返回一個類物件,該類含有可呼叫物件的資料成員。如果函式返回一個lambda,則與函式不能返回一個區域性變數的引用類似,此lambda也不能包含引用捕獲。

3:隱式捕獲

    除了顯式列出我們希望使用的來自所在函式的變數之外,還可以讓編譯器根據lambda體中的程式碼來推斷我們要使用哪些變數。為了指示編譯器推斷捕獲列表,應在捕獲列表中寫一個” &” 或”=”。 ” &”告訴編譯器採用引用捕獲方式,”=”則表示採用值捕獲方式。例如:

we = find_if(words.begin(), words.end(), 
             [=](const string &s)
              { return s.size() >= sz; });

 如果希望對一部分變數採用值捕獲,對其他變數採用引用捕獲,可以混合使用隱式捕獲和顯式捕獲:

void biggies(vector<string> &words, vector<string>::size_ type sz,
             ostream &os=cout, char c=' ')
{
    //os隱式捕獲,引用捕獲方式;c顯式捕獲,值捕獲方式
    for_each(words.begin(), words.end(), 
              [&, c](const strinq &s) { os << s << c; });
              
    //os顯式捕獲,引用捕獲方式;c隱式捕獲,值捕獲方式
    for_each(words.begin(), words.end(), 
              [=, &os](const strinq &s) { os << s << c; });
}

 當混合使用隱式捕獲和顯式捕獲時,捕獲列表中的第一個元素必須是一個”&”或”=“。此符號指定了預設捕獲方式為引用或值;並且顯式捕獲的變數必須使用與隱式捕獲不同的方式。即,如果隱式捕獲是引用方式,則顯式捕獲命名變數必須採用值方式;類似的,如果隱式捕獲採用的是值方式,則顯式捕獲命名變數必須採用引用方式。

 

 

二:可變lambda

預設情況下,對於一個按值捕獲的變數,lambda不能改變其值。如果希望能改變這個被捕獲的變數的值,就必須在引數列表之後加上關鍵字mutable,因此,可變lambda不能省略引數列表:

int v1 = 42;
auto f=[v1] () mutable {return ++v1;};
v1=0;
auto j = f(); //j is 43

     一個引用捕獲的變數是否可以修改依賴於此引用指向的是一個const型別還是一個非const型別:

int v1 = 42;
auto f=[&v1] () {return ++v1;};
v1=0;
auto j = f(); //j is 1