c++ - lambda教學 - lambda用法




在C++ 11 lambda中通過引用捕獲引用 (2)

考慮一下:

#include <functional>
#include <iostream>

std::function<void()> make_function(int& x) {
    return [&]{ std::cout << x << std::endl; };
}

int main() {
    int i = 3;
    auto f = make_function(i);
    i = 5;
    f();
}

該程序是否保證輸出5而不調用未定義的行為?

我理解它是如何工作的,如果我通過值( [=] )捕獲x ,但我不確定我是否通過引用捕獲它來調用未定義的行為。 可能是我在make_function返回後make_function一個懸空引用,或者只要最初引用的對象仍然存在,捕獲的引用是否可以保證工作?

在這裡尋找明確的基於標準的答案:) 到目前為止它在實踐中運作良好;)


TL; DR:標準不保證問題中的代碼,並且有合理的lambdas實現會導致它中斷。 假設它是非便攜式的而是使用

std::function<void()> make_function(int& x)
{
    const auto px = &x;
    return [/* = */ px]{ std::cout << *px << std::endl; };
}

從C ++ 14開始,您可以使用初始化捕獲來消除顯式使用指針,這會強制為lambda創建新的引用變量,而不是重用封閉範圍中的那個:

std::function<void()> make_function(int& x)
{
    return [&x = x]{ std::cout << x << std::endl; };
}

乍一看,似乎應該是安全的,但標準的措辭會引起一些問題:

lambda表達式是一個局部lambda表達式,其最小的封閉範圍是塊作用域(3.3.3); 任何其他lambda-expression在其lambda-introducer中都不應該有capture-default或simple-capture。 本地lambda表達式的到達範圍是包含範圍的集合,包括最裡面的封閉函數及其參數。

...

所有這些隱式捕獲的實體都應在lambda表達式的到達範圍內聲明。

...

[注意:如果通過引用隱式或顯式捕獲實體,則在實體的生命週期結束後調用相應lambda表達式的函數調用運算符可能會導致未定義的行為。 - 結束說明]

我們期望發生的是,在make_function使用的x指的是main() i (因為這是引用所做的),並且實體i是通過引用捕獲的。 由於該實體仍然生活在lambda呼叫時,一切都很好。

但! “隱式捕獲的實體”必須“在lambda表達式的範圍內”,並且main() i不在達到範圍內。 :(除非參數x計為“在達到範圍內聲明”,即使實體i本身在達到範圍之外。

這聽起來像是, 與C ++中的任何其他地方不同,創建了引用引用,並且引用的生命週期具有意義。

絕對是我希望看到標準澄清的東西。

同時,TL; DR部分中顯示的變體肯定是安全的,因為指針是通過值捕獲的(存儲在lambda對象本身內),並且它是指向持續通過lambda調用的對象的有效指針。 我還希望通過引用捕獲實際上最終會存儲一個指針,因此執行此操作不應該有運行時懲罰。

經過仔細檢查,我們也可以想像它可能會破裂。 請記住,在x86上,在最終的機器代碼中,使用EBP相對尋址訪問局部變量和函數參數。 參數具有正偏移,而本地為負。 (其他體系結構具有不同的寄存器名稱,但許多體系結構以相同的方式工作。)無論如何,這意味著可以通過僅捕獲EBP的值來實現按引用捕獲。 然後可以通過相對尋址再次找到本地和參數。 事實上,我相信我已經聽說過Lambda實現(在C ++之前很久就有lambdas的語言)正是這樣做的:捕獲lambda定義的“堆棧幀”。

這意味著當make_function返回並且其堆棧框架消失時,所有訪問本地和參數的能力也是如此,即使是那些引用也是如此。

標準包含以下規則,可能專門用於實現此方法:

未指定是否在閉包類型中為通過引用捕獲的實體聲明了其他未命名的非靜態數據成員。

結論:標準中不保證問題中的代碼,並且有合理的lambdas實現會導致它破壞。 假設它是不可移植的。


代碼保證有效。

在我們深入研究標準措辭之前:這是C ++委員會的意圖,即此代碼的工作原理。 然而,目前的措辭被認為是不夠明確的(實際上,對標准後C ++ 14的錯誤修正打破了使其有效的微妙安排),因此提出了2011CWG問題澄清事項,現在正在通過委員會。 據我所知,沒有實現得到這個錯誤。

我想澄清一些事情,因為Ben Voigt的答案包含一些造成一些混淆的事實錯誤:

  1. “範圍”是C ++中的靜態詞彙概念,它描述了程序源代碼的一個區域,其中非限定名稱查找將特定名稱與聲明相關聯。 它與生命無關。 見[basic.scope.declarative]/1
  2. 同樣,lambdas的“達到範圍”規則也是一種語法屬性,用於確定何時允許捕獲。 例如:

    void f(int n) {
      struct A {
        void g() { // reaching scope of lambda starts here
          [&] { int k = n; };
          // ...
    

    n在此範圍內,但lambda的範圍不包括它,因此無法捕獲。 換句話說,lambda的達到範圍是它可以到達並捕獲變量的“向上”多遠 - 它可以達到封閉(非lambda)函數及其參數,但它無法到達外部和捕獲出現在外面的聲明。

所以“達到範圍”的概念與這個問題無關。 被捕獲的實體是make_function的參數x ,它在lambda的範圍內。

好的,讓我們來看看標准在這個問題上的措辭。 Per [expr.prim.lambda] / 17,只有引用副本捕獲的實體的id-expression被轉換為lambda閉包類型的成員訪問; id-expression s引用通過引用捕獲的實體是單獨的,並且仍然表示它們將在封閉範圍中表示的相同實體。

這看起來很糟糕:參考x的生命週期已經結束,那麼我們怎麼能參考呢? 好吧,事實證明,幾乎(見下文)沒有辦法在其生命週期之外引用引用(你可以看到它的聲明,在這種情況下它在範圍內,因此可以使用,或者它是一個類成員,在這種情況下,類本身必須在其生命週期內,以使成員訪問表達式有效)。 因此,該標准直到最近才禁止在其生命週期之外使用參考。

lambda措辭利用了這樣一個事實,即在其生命週期之外使用引用沒有任何懲罰,因此不需要為通過引用意味著捕獲的實體的訪問提供任何明確的規則 - 它只是意味著你使用它實體; 如果它是引用,則名稱表示其初始化程序。 這就是如何保證直到最近才開始工作(包括在C ++ 11和C ++ 14中)。

但是,你不能在其生命週期之外提及參考,這是不正確的; 特別是,您可以從它自己的初始化程序中引用它,從引用之前的類成員的初始化程序引用它,或者如果它是名稱空間範圍變量,並且您從另一個在它之前初始化的全局訪問它。 引入了CWG 2012年的問題來修復這種疏忽,但它無意中通過引用參考來破壞了lambda捕獲的規範。 我們應該在C ++ 17發布之前修復這個回歸; 我已經提交了一份國家機構評論,以確保其優先順序。







language-lawyer