2

我的實際問題是複雜得多,看起來很難給出一個簡短的具體示例來重現它。所以我在這裏發帖不同的小例子,可能是相關的,並且它的討論可以在實際問題有所幫助:嵌套std :: forward_as_tuple和分段錯誤

// A: works fine (prints '2') 
cout << std::get <0>(std::get <1>(
    std::forward_as_tuple(3, std::forward_as_tuple(2, 0))) 
) << endl; 

// B: fine in Clang, segmentation fault in GCC with -Os 
auto x = std::forward_as_tuple(3, std::forward_as_tuple(2, 0)); 
cout << std::get <0>(std::get <1>(x)) << endl; 

的實際問題不涉及std::tuple,所以做出的例子獨立的,這裏有一個定製,最小大致相當於:

template <typename A, typename B> 
struct node { A a; B b; }; 

template <typename... A> 
node <A&&...> make(A&&... a) 
{ 
    return node <A&&...>{std::forward <A>(a)...}; 
} 

template <typename N> 
auto fst(N&& n) 
-> decltype((std::forward <N>(n).a)) 
    { return std::forward <N>(n).a; } 

template <typename N> 
auto snd(N&& n) 
-> decltype((std::forward <N>(n).b)) 
    { return std::forward <N>(n).b; } 

根據這些定義,我得到完全相同的行爲:

// A: works fine (prints '2') 
cout << fst(snd(make(3, make(2, 0)))) << endl; 

// B: fine in Clang, segmentation fault in GCC with -Os 
auto z = make(3, make(2, 0)); 
cout << fst(snd(z)) << endl; 

一般來說,APPEA rs的行爲取決於編譯器和優化級別。我無法通過調試找到任何東西。看來在所有情況下,所有內容都被內聯和優化,所以我無法弄清楚導致問題的特定代碼行。

如果臨時表只要存在對它們的引用(並且我沒有從函數體內返回對局部變量的引用),就沒有看到上述代碼可能導致問題的根本原因和爲什麼案例A和B應該有所不同。

在我的實際問題中,即使對於單線版本(情況A),Clang和GCC都給出了分割錯誤,並且不管優化級別如何,所以問題相當嚴重。

使用值或rvalue引用(例如,在自定義版本中爲std::make_tuplenode <A...>)時,問題消失。當元組不嵌套時,它也會消失。

但是以上都沒有幫助。我正在實現的是一種用於視圖的表達式模板,可以對許多結構進行延遲評估,包括元組,序列和組合。所以我肯定需要對臨時對象的右值引用。一切工作正常嵌套元組,例如對於嵌套操作的表達式,例如(a, (b, c))u + 2 * v,但不是兩者。

我希望能夠幫助理解上面的代碼是否有效,如果預計會出現分段錯誤,如何避免它,以及編譯器和優化級別會發生什麼。

回答

1

這裏的問題是聲明「如果臨時對象只要有引用就應該存在」。這隻有在有限的情況下才是真實的,你的計劃並不能證明其中的一種情況。您正在存儲一個元組,其中包含臨時表的引用,這些臨時表在完整表達式的末尾被銷燬。此程序演示得很清楚(Live code at Coliru):

struct foo { 
    int value; 
    foo(int v) : value(v) { 
     std::cout << "foo(" << value << ")\n" << std::flush; 
    } 
    ~foo() { 
     std::cout << "~foo(" << value << ")\n" << std::flush; 
    } 
    foo(const foo&) = delete; 
    foo& operator = (const foo&) = delete; 
    friend std::ostream& operator << (std::ostream& os, 
             const foo& f) { 
     os << f.value; 
     return os; 
    } 
}; 

template <typename A, typename B> 
struct node { A a; B b; }; 

template <typename... A> 
node <A&&...> make(A&&... a) 
{ 
    return node <A&&...>{std::forward <A>(a)...}; 
} 

template <typename N> 
auto fst(N&& n) 
-> decltype((std::forward <N>(n).a)) 
    { return std::forward <N>(n).a; } 

template <typename N> 
auto snd(N&& n) 
-> decltype((std::forward <N>(n).b)) 
    { return std::forward <N>(n).b; } 

int main() { 
    using namespace std; 
    // A: works fine (prints '2') 
    cout << fst(snd(make(foo(3), make(foo(2), foo(0))))) << endl; 

    // B: fine in Clang, segmentation fault in GCC with -Os 
    auto z = make(foo(3), make(foo(2), foo(0))); 
    cout << "referencing: " << flush; 
    cout << fst(snd(z)) << endl; 
} 

,因爲它訪問存儲在元組在同一個充分體現引用A工作正常,因爲它存儲的元組和訪問引用後B是未定義行爲。請注意,although it may not crash when compiled with clang,它顯然是未定義的行爲,但由於在其生命週期結束後訪問對象。

如果你想使這個使用安全的,你可以很容易地改變節目存儲到左值引用,但移動右值到元組本身(Live demo at Coliru):

template <typename... A> 
node<A...> make(A&&... a) 
{ 
    return node<A...>{std::forward <A>(a)...}; 
} 

更換node<A&&...>node<A...>是絕招:由於A是一個通用的引用,所以A的實際類型將是左值參數的左值引用,以及左值參數的非參考類型。參考摺疊規則對我們的這種使用以及完美轉發有利。

編輯:至於爲什麼此方案中的臨時工沒有它們的壽命延長到引用的壽命,我們來看看C++ 11個12.2臨時對象[class.temporary]第4款:

有兩種情況,臨時狀態在與完整表達式結尾不同的點處被銷燬。第一個上下文是當調用默認構造函數來初始化數組的元素時。如果構造函數有一個或多個默認參數,則在構造下一個數組元素(如果有)之前,對在默認參數中創建的每個臨時對象的銷燬都進行排序。

更多地參與第5段:

的第二上下文是當引用綁定到一個暫時的。臨時到該參考結合或臨時即其上結合的參考持續基準的除了壽命的子對象的完整的對象:

  • 臨時結合到在基準部件構造函數的ctor初始值設定項(12.6.2)一直存在,直到 構造函數退出。

  • 臨時綁定到函數調用中的引用參數(5.2.2),直到完成包含調用的全表達式爲 。

  • 函數返回語句(6.6.3)中返回值的臨時綁定的生存期不是 擴展;在return語句的完整表達式的末尾臨時被銷燬。

  • 臨時結合到在新初始化(5.3.4)的參考持續直到含有新初始化全表達的完成。 [實施例:

struct S { int mi; const std::pair<int,int>& mp; }; 
S a { 1, {2,3} }; 
S* p = new S{ 1, {2,3} }; // Creates dangling reference 

末端示例] [注:這可能引入一個懸空參考,並鼓勵實現在這樣的情況下發出警告。 - 注意]

銷燬一個臨時的,其生命週期不被綁定到一個引用後被排序,然後破壞在同一個完整表達式中構造的每個臨時對象。如果兩個或兩個以上臨時參照物的臨界壽命結束於同一點,則這些臨時物體將按照與其建造完成相反的順序銷燬。此外,銷燬與參考文件相關的臨時文件時應考慮使用靜態,線程或自動存儲時間(3.7.1,3.7.2,3.7.3)銷燬對象 的順序;也就是說,如果obj1是臨時存儲時間與臨時存儲持續時間相同的對象,並且在創建臨時存儲之前創建的臨時存儲應在銷燬obj1之前銷燬;如果obj2是與臨時存儲時間相同且臨時創建後創建的對象,臨時應在銷燬obj2後銷燬。 [實施例:

struct S { 
    S(); 
    S(int); 
    friend S operator+(const S&, const S&); 
    ~S(); 
}; 
S obj1; 
const S& cr = S(16)+S(23); 
S obj2; 

表達S(16) + S(23)創建三個臨時變量:第一臨時T1保持表達S(16),第二臨時T2的結果來保持表達S(23)的結果,以及第三臨時T3來保存添加這兩個表達式的結果。然後將T3臨時綁定到參考號cr。未指定是首先創建T1還是T2。在T2之前創建T1的實施中,確保T2T1之前銷燬。臨時參數T1T2綁定到參考參數operator+;這些臨時對象在包含operator+的調用的完整表達式的末尾被銷燬。 cr的臨時T3cr的生命週期結束時被銷燬, 即在程序結束時被銷燬。另外,銷燬T3的順序考慮了具有靜態存儲持續時間的其他對象的銷燬順序。即,由於是obj1之前T3構造,並且T3構造obj2之前,可以保證obj2被T3之前破壞,並且T3obj1之前破壞。 - 結束示例]

您正在綁定臨時「到構造函數的ctor初始值設定項中的引用成員」。

+0

非常感謝您的回覆。這真是太神奇了,我甚至都沒有想過這種調試......總之,我有幾個問題,一次一個:(1)'int && x = 3怎麼樣; cout << x << endl;'?這是「有限的情況」還是未定義的?因爲它不僅工作正常,而且我也在Stroustrup的書中看到了它。如果它是有效的,那麼我的例子有什麼不同?是不是現在這個'int &&'在另一個結構中?我不明白爲什麼它會有所不同。 – iavr

+0

問題(2):我在想,儘管更復雜,但我的'實際問題'很可能是由於這個問題(儘管程序在單一表達式上崩潰),並且您的解決方案似乎是合理的。但是,我關心的不僅僅是左值引用;它也是關於非引用類型的,在這種情況下,它將按值傳遞和存儲。這些可能很大,並會在單個表達式中傳遞/複製/移動數次。因此,除了在免費商店中存儲數據並移動構造函數的對象之外,我是否僅僅依靠副本來避免複製? – iavr

+0

用延長壽命的東西擴大了答案。總之(1)是的,那是「有限的情況」。它與您的示例不同之處在於它將臨時性直接綁定到引用,而不是對象的引用成員。 (2)是的,如果您通過右值引用傳遞昂貴移動的對象,將會代價高昂。如果你不能避免這種情況,你可以通過回到純引用來改變設計,並引入另一個「存儲」類,當從引用元組分配時,該類會吞噬右值。 – Casey