2012-04-09 61 views
4

該標準是否定義了此代碼會發生什麼?在C++ lambda表達式中通過值捕獲是否需要將值與lambda對象一起復制?

#include <iostream> 

template <typename Func> 
void callfunc(Func f) 
{ 
    ::std::cout << "In callfunc.\n"; 
    f(); 
} 

template <typename Func> 
void callfuncref(Func &f) 
{ 
    ::std::cout << "In callfuncref.\n"; 
    f(); 
} 

int main() 
{ 
    int n = 10; 
    // n is captured by value, and the lambda expression is mutable so 
    // modifications to n are allowed inside the lambda block. 
    auto foo = [n]() mutable -> void { 
     ::std::cout << "Before increment n == " << n << '\n'; 
     ++n; 
     ::std::cout << "After increment n == " << n << '\n'; 
    }; 
    callfunc(foo); 
    callfunc(foo); 
    callfuncref(foo); 
    callfunc(foo); 
    return 0; 
} 

的這個輸出與G ++是:

$ ./a.out 
In callfunc. 
Before increment n == 10 
After increment n == 11 
In callfunc. 
Before increment n == 10 
After increment n == 11 
In callfuncref. 
Before increment n == 10 
After increment n == 11 
In callfunc. 
Before increment n == 11 
After increment n == 12 

是否要求由標準此輸出的所有特徵?

特別是,如果創建了一個lambda對象的副本,則所有捕獲的值也會被複制。但是,如果lambda對象通過引用傳遞,則不會複製捕獲的值。並且在函數被調用之前,沒有任何副本是由捕獲的值組成的,因此在調用之間會保留捕獲值的變化。

回答

9

lambda的類型只是一個類(n3290§5.1.2/ 3),其中一個執行主體(/ 5)的operator()和一個隱式複製構造函數(/ 19),並通過捕獲變量複製相當於將它複製 - 初始化(/ 21)到此類的非靜態數據成員(/ 14),並且每個用該變量的對象都被相應的數據成員(/ 17)替代。在這個轉換之後,lambda表達式只成爲這個類的一個實例,並且遵循C++的一般規則。

這意味着,你的代碼應以相同的方式工作爲:

int main() 
{ 
    int n = 10; 

    class __Foo__   // §5.1.2/3 
    { 
     int __n__;   // §5.1.2/14 
    public: 
     void operator()() // §5.1.2/5 
     { 
      std::cout << "Before increment n == " << __n__ << '\n'; 
      ++ __n__;  // §5.1.2/17 
      std::cout << "After increment n == " << __n__ << '\n'; 
     } 
     __Foo__() = delete; 
     __Foo__(int n) : __n__(n) {} 
     //__Foo__(const __Foo__&) = default; // §5.1.2/19 
    } 
    foo {n};    // §5.1.2/21 

    callfunc(foo); 
    callfunc(foo); 
    callfuncref(foo); 
    callfunc(foo); 
} 

而且很明顯這裏什麼callfuncref一樣。

+0

這些lambda等價類是否也有移動構造函數和移動賦值操作? – Omnifarious 2012-04-09 14:01:09

+3

@Omnifarious:移動構造函數:「Maybe」(§5.1.2/ 19:「可能有一個隱式聲明的移動構造函數」)。 lambda沒有任何賦值操作符,它們被刪除。 – kennytm 2012-04-09 14:08:53

2

除非您明確捕獲所有[&]或通過引用[&n]捕獲特定變量,否則該值通過複製捕獲。所以整個輸出是標準的。

+0

我特別感興趣的是'callfuncref'。這與捕獲值的可變性有關,而不是通過值或引用捕獲是否爲默認值。 – Omnifarious 2012-04-09 13:15:15

4

我覺得最容易理解這種行爲,通過手動擴展lambda到一個結構/類,這將是或多或少像這樣(如n被值捕獲,通過引用捕獲看起來有點不同):

class SomeTemp { 
    mutable int n; 
    public: 
    SomeTemp(int n) : n(n) {} 
    void operator()() const 
    { 
     ::std::cout << "Before increment n == " << n << '\n'; 
     ++n; 
     ::std::cout << "After increment n == " << n << '\n'; 
    } 
} foo(n); 

你的功能callfunccallfuncref操作上此類型的對象更多或更少。現在,讓我們看看你做的呼叫:

callfunc(foo); 

在這裏,您按值傳遞,所以foo將使用默認的拷貝構造函數複製。 callfunc中的操作只會影響複製值的內部狀態,實際foo對象中沒有狀態更改。

callfunc(foo); 

同樣的東西

callfuncref(foo); 

啊,我們在這裏通過引用傳遞foo的,所以callfuncref(它調用operator())將更新的實際foo對象,而不是一個臨時副本。這將導致foon事件後更新爲11,這是常規的pass-by-reference行爲。因此,當您再次調用此:

callfunc(foo); 

您將在副本上再次操作,但foo副本,其中n被設置爲11這表明你的期望。

+0

在另一個問題中,我並不十分關心標準所說的內容,我會認爲你的答案是更好的答案。 :-) – Omnifarious 2012-04-09 14:52:57