[英]Temporary lifetime and perfect forwarding constructor
我無法理解為什么當有一個完美的轉發構造函數時,綁定到const參考參數的臨時工的生命周期被縮短。 首先,我們知道有關臨界參數的臨時參數:它們持續完整表達式:
在函數調用(5.2.2)中與引用參數的臨時綁定將持續存在,直到包含該調用的完整表達式完成為止
但是我發現這種情況並非如此(或者我可能只是誤解了完整表達式是什么)。 讓我們舉一個簡單的例子,首先我們用詳細的構造函數和析構函數定義一個對象:
struct A {
A(int &&) { cout << "create A" << endl; }
A(A&&) { cout << "move A" << endl; }
~A(){ cout << "kill A" << endl; }
};
還有一個對象包裝器B,它將用於參考折疊:
template <class T> struct B {
T value;
B() : value() { cout << "new B" << endl; }
B(const T &__a) : value(__a) { cout << "create B" << endl; }
B(const B &p) = default;
B(B && o) = default;
~B(){ cout << "kill B" << endl; };
};
我們現在可以使用我們的包裝器來捕獲臨時對象的引用並在函數調用中使用它們,如下所示:
void foo(B<const A&> a){ cout << "Using A" << endl; }
int main(){ foo( {123} ); }
上面的程序打印出我期望的內容:
create A
create B
Using A
kill B
kill A
到現在為止還挺好。 現在讓我們回到B
並為可轉換類型添加一個完美的轉發構造函數:
template <class T> struct B {
/* ... */
template <class U, class = typename enable_if<is_convertible<U, T>::value>::type>
B(U &&v) : value(std::forward<U>(v)) {
cout << "new forward initialized B" << endl;
}
};
現在再次編譯相同的代碼給出:
create A
new forward initialized B
kill A
Using A
kill B
請注意,我們的A
對象現在在使用之前被殺死了,這很糟糕! 在這種情況下,為什么臨時的生命周期沒有擴展到foo
的完整調用? 此外,沒有其他調用A
的析構函數,因此沒有其他實例。
我可以看到兩種可能的解釋:
B(T &&v)
而不是template <class U>B(U &&v)
解決問題。 {123}
不是foo( {123} )
的子表達式。 交換{123}
的A(123)
也解決了這個問題,這讓我想知道大括號初始化器是否是完整的表達式。 有人可以澄清這里發生了什么嗎?
這是否意味着在某些情況下向類添加轉發構造函數可能會破壞向后兼容性,就像它對B
?
你可以在這里找到完整的代碼,另一個測試用例崩潰以引用字符串。
在對B<A const&>::B(U&&)
的調用中推斷為U
的類型是int
,因此在main
調用foo
的唯一臨時可以是生命周期擴展的是初始化為123
的prvalue int
臨時。
成員A const& value
綁定到臨時A
,但是A
在構造函數B<A const&>::B(U&&)
的mem-initializer-list中創建,因此其生命周期僅在該成員的持續時間內延長初始化[class.temporary] / 5:
- 構造函數的ctor-initializer (12.6.2)中的引用成員的臨時綁定將持續存在,直到構造函數退出。
請注意, mem-initializer-list是ctor-initializer中冒號后面的部分:
template <class U, class = typename enable_if<is_convertible<U, T>::value>::type>
B(U &&v) : value(std::forward<U>(v)) {
^--- ctor-initializer
^--- reference member
^--- temporary A
這就是為什么在 new forward initialized B
之后打印kill A
這是否意味着在某些情況下向類添加轉發構造函數可能會破壞向后兼容性,就像它對
B
?
是。 在這種情況下,很難理解為什么轉發構造函數是必要的; 如果你有一個臨時可以綁定的引用成員,那肯定是危險的。
void foo(B<const A&> b);
foo( {123} );
在語義上等同於:
B<const A&> b = {123};
非顯式構造函數在語義上等效於:
B<const A&> b{123};
更進一步,因為你的轉發構造函數需要任何東西,它實際上是用int
初始化的,而不是A
:
B<const A&>::B(int&& v)
也就是說,在構造函數的初始化列表上創建了一個臨時A
實例:
B(int&& v) : value(A{v}) {}
// created here ^ ^ destroyed here
這是合法的 ,就像你可以鍵入const A& a{123};
。
這A
實例后銷毀B
的建設完成,並且你最終的體內的懸掛引用 foo
。
在調用表達式中構建實例時,情況會發生變化,然后A
臨時在調用表達式結束時結束其生命周期:
foo( A{123} );
// ^ A is destroyed here
因此它在foo
保持活動狀態,並且為B<const A&>
選擇的轉發構造 B<const A&>
用類型A&&
實例化。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.