[英]Capturing a reference by reference in a C++11 lambda
考慮一下:
#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:問題中的代碼不受標准的保證,並且有合理的lambda實現會導致其中斷。 假設它是非便攜式的,而是使用
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; };
}
乍看之下,似乎應該是安全的,但是該標准的措詞會引起一些問題:
最小包圍范圍是塊范圍(3.3.3)的lambda表達式是局部lambda表達式。 其他任何lambda表達式在其lambda引入程序中均不得具有默認捕獲或簡單捕獲功能。 局部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 ++之前有lambda的語言中)正是這樣做的:捕獲定義lambda的“堆棧框架”。
這意味着當make_function
返回並且其堆棧幀消失時,所有訪問本地AND參數的能力也將消失,即使是那些引用。
該標准包含以下規則,可能專門用於啟用此方法:
對於通過引用捕獲的實體,在關閉類型中是否聲明了其他未命名的非靜態數據成員,尚不確定。
結論:該問題中的代碼不受標准的保證,並且有合理的lambda實現會導致其中斷。 假設它是不可攜帶的。
該代碼保證有效。
在我們深入研究標准措辭之前:C ++委員會的意圖是該代碼有效。 但是,據信目前的措詞尚不清楚(實際上,對標准C ++ 14之后的錯誤修正破壞了使其起作用的細致安排),因此提出了CWG 2011 ,以澄清問題,並正在通過委員會審議。 據我所知,沒有實現會出錯。
我想澄清幾件事,因為Ben Voigt的答案包含一些事實錯誤,這些錯誤正在引起混淆:
同樣,lambda的“到達范圍”規則是一種語法屬性,它確定何時允許捕獲。 例如:
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的范圍內。
好的,讓我們來看一下該標准的措辭。 根據[expr.prim.lambda] / 17,只有引用被副本捕獲的實體的id-expressions會轉換為lambda閉包類型的成員訪問權限; 引用由引用捕獲的實體的id-expressions保留不變,並且仍表示它們在封閉范圍內應表示的同一實體。
這立即看起來很糟糕:引用x
的生命周期已經結束,那么我們如何引用它呢? 好吧,事實證明,幾乎沒有辦法(參見下文)在其生命周期之外引用該引用(您可以看到它的聲明,在這種情況下它在范圍內,因此可以使用,或者它是一個類成員,在這種情況下,類本身必須在其生存期內,成員訪問表達式才有效)。 結果,直到最近,該標准才沒有禁止在其生存期內使用參考的任何限制。
Lambda措辭利用了這樣一個事實,即在其生存期之外使用引用不會受到任何懲罰,因此不需要為通過引用方式捕獲的實體的訪問權限提供任何明確的規則-這只是意味着您使用了實體; 如果是引用,則名稱表示其初始化程序。 這就是保證可以一直使用到最近(包括C ++ 11和C ++ 14)的方式。
然而,這並不完全正確,你可以不提它的生命周期之外的參考; 特別是,您可以從其自己的初始化程序中,在引用之前的類成員的初始化程序中引用它,或者如果它是一個命名空間范圍變量,並且可以從另一個在其之前初始化的全局變量中訪問它,則可以對其進行引用。 引入了CWG 2012年版來解決此問題 ,但無意間破壞了參考引用的lambda捕獲規范。 我們應該在C ++ 17發行之前解決此回歸問題; 我已經提交了國家機構評論,以確保其優先級適當。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.