2016-03-03 134 views
4

我用自定義迭代器編寫了一個自定義容器。由於容器的特定功能,迭代器必須被懶惰地評估。對於這個問題的緣故代碼的相關部分是迭代的對其操作它以這種方式實現編譯器優化打破了懶惰的迭代器

template<typename T> 
struct Container 
{ 
    vector<T> m_Inner; 

    // This should calculate the appropriate value. 
    // In this example is taken from a vec but in 
    //the real use-case is calculated on request 
    T Value(int N) 
    { m_Inner.at(N); } 
} 

template<typename T> 
struct Lazy_Iterator 
{ 
    mutable pair<int, T> m_Current; 
    int Index 
    Container<T>* C 

    Lazy_Iterator(const Container& Cont, int N): 
    m_Current{Index, T{}}, Index{N}, C{&Cont} 
    {  } 

    pair<int, T>& 
    operator*() const // __attribute__((noinline)) (this cures the symptom) 
    { 
     m_Current.first = Index; /// Optimized out 
     m_Current.second = C->Value(Index); /// Optimized out 
     return m_Current; 
    } 

} 

因爲迭代器本身是一個模板,它的功能可以自由通過編譯器內聯。

當我沒有優化編譯代碼時,返回的值按預期更新。當我使用發佈編譯器優化(在GCC 4.9中爲-O2)時,在某些情況下,即使m_Current成員標記爲可變,編譯器也會優化我標記爲優化出的的行。因此返回值與迭代器應該指向的值不匹配。

這是預期的行爲?你知道任何可移植的方式來指定該函數的內容應該被評估,即使它被標記爲const嗎?

我希望這個問題足夠詳盡,有用。如果在這種情況下更多細節可能會有所幫助,請諮詢。

編輯:

要回答一個評論,這是從一個小的測試程序採取了潛在的使用:

Container<double> myC; 
Lazy_Iterator<double> It{myC, 0} 
cout << "Creation: " << it->first << " , " << it->second << endl; 

auto it2 = it; 
cout << "Copy: "<< it2->first << " , " << it2->second << endl; 

cout << "Pre-increment: " << (it++)->first << " , " << it->second << endl; 
cout << "Post-increment: " << (++it)->first << " , " << it->second << endl; 
cout << "Pre-decrement: " << (it--)->first << " , " << it->second << endl; 
cout << "Post-decrement: " << (--it)->first << " , " << it->second << endl; 
cout << "Iterator addition: " << (it+2)->first << " , " << (it+2)->second << endl; 
cout << "Iterator subtraction: "<< (it-2)->first << " , " << (it-2)->second << endl; 

reverse_iterator<Lazy_Iterator> rit{it}; 
cout << "Reverse Iterator: " << rit->first << " , " << rit->second << endl; 

auto rit2 = rit; 
cout << "Reverse Iterator copy: " << rit2->first << " , " << rit2->second << endl; 

cout << "Rev Pre-increment: " << (rit++)->first << " , " << rit->second << endl; 
cout << "Rev Post-increment: " << (++rit)->first << " , " << rit->second << endl; 
cout << "Rev Pre-decrement: " << (rit--)->first << " , " << rit->second << endl; 
cout << "Rev Post-decrement: " << (--rit)->first << " , " << rit->second << endl; 
cout << "Rev Iterator addition: " << (rit+2)->first << " , " << (rit+2)->second << endl; 
cout << "Rev Iterator subtraction: "<< (rit-2)->first << " , " << (rit-2)->second << endl; 

測試結果是否如預期般對所有測試除了最後兩行

開啓優化時,測試的最後兩行發生故障。

該系統實際上運行良好,並不比其他迭代器更危險。當然,如果容器在他的鼻子下被刪除,它可能會失敗,並且通過複製使用返回的值可能會更安全,而不僅僅是保留參考,但這是脫離主題

+1

這是一個很好的問題,但是您認爲您可以編輯代碼片段來獲取錯別字嗎? – Bathsheba

+0

你指的是哪種拼寫錯誤?我改變了我忘記替換的typedefs的類型。如果有更多,請讓我知道 – Triskeldeian

+0

你能提供[mcve]嗎?目前在問題中的代碼看起來是正確的。 – Angew

回答

2

有與reverse_iterator(什麼是.base()返回)舉行物理迭代器和邏輯值之間的差異問題,它指出:他們的off-by -一。 reverse_iterator might do return *(--internal_iterator); on dereference,這會讓您對被銷燬的函數本地臨時內部的懸掛引用進行引用。

經過另一次閱讀標準後,我發現它有額外的要求,以避免這種情況,閱讀說明。

另外我發現GCC 4.9標準庫不符合標準。它使用臨時的。所以,我認爲這是一個GCC錯誤。

編輯:標準報價

24.5.1.3.4運算符* [reverse.iter.op.star]

reference operator*() const; 

效果:

deref_tmp = current; 
--deref_tmp; 
return *deref_tmp; 

[注意:此操作必須使用輔助成員變量而不是臨時變量,以避免返回超出其關聯迭代器生命週期的引用。 (見24.2。) - 端注]

後續閱讀: Library Defect Report 198

it seems它是returned to old behaviour

延遲編輯:P0031被選爲C++ 17工作草案。它指出reverse_iterator使用臨時的,不是成員來保存中間值。

+0

這看起來很有趣。但是不會引起訪問衝突嗎? 爲什麼這隻在優化打開時纔會出現? – Triskeldeian

+0

@Triskeldeian這是UB,但它通常不會導致崩潰。它仍然指向程序擁有的一些內存,但是誰知道這個內存中寫的是什麼。由於局部變量放置在幾乎任何事情都使用的堆棧上,只要函數退出,內存就會被覆蓋。 –

+0

非常感謝。我認爲你擊中了靶心。直到知道我總是用T作爲一些數字類型進行測試。我切換到std :: string給它一個嘗試,現在,即使優化脫離反向迭代器的解引用的返回值是亂碼。這是否意味着我應該編寫自己的逆向迭代器,或者您是否知道解決此問題的一些好技術? – Triskeldeian

1

如果您必須發佈可編譯的代碼段這再現了這個問題(實際上我無法用GCC 4.9重現它)。我認爲你有未定義的行爲,並且是由O2(O2啓用可以打破未定義行爲的優化)觸發的。你應該有一個指針,指向

Container<T> 

裏面的迭代器。

反正要知道,一個懶惰的迭代器打破std的迭代器的合同,我想一個更好的選擇是讓懶值的定期集裝箱,你可以這樣跳到創建一個自定義容器和共迭代器) (看代理模式)。

+0

他的確如此。指針是'C' – MSalters

+0

我沒有忘記模板參數。我糾正了它。 我還在測試代碼中添加了更多行。如果進行優化,那麼失敗的測試是最後兩個,即反向迭代器的算術。在這種情況下,應該解除對引用內部的前向迭代器的副本的解引用,並且在該引用期間迭代器的內容的更新似乎被刪除 – Triskeldeian

2

「優化掉了,即使m_Current成員被標記爲可變

這告訴我,你是假設優化關心mutable。它沒有。 constmutable已被較早的編譯階段剝離。

爲什麼優化器刪除了兩條語句?如果它們被內聯?我懷疑在內聯之後,優化器可以證明這兩個寫操作是無操作的,要麼m_Current變量必須保存正確的值,,因爲後續使用m_Current使其沒有實際意義。平凡下列情況下使這些寫入無操作:

Lazy_Iterator LI = foo(); // Theoretically writes 
*LI = bar(); // Overwrites the previous value. 
+0

如果在優化期間編譯器忽略存儲在類可以改變,因爲在此期間只有被調用的const成員函數,那麼它可能是有意義的,但是這會假設優化器不知道數據成員的常量,但知道一個常量方法。此外,我試圖從操作員和可變成員中刪除const,但沒有變化 – Triskeldeian

+0

@Triskeldeian:同樣,優化器甚至沒有那個'const member'信息,也不會有幫助。你的'T const * this'可能無法修改成員變量,但是可能存在一個全局的'T * singleton',它以非const的方式對這個對象進行了別名化。或者可能有一個全局'int&'別名'this'。優化器必須**證明**沒有可能的併發訪問。 – MSalters

+0

這就是我在評論中的意思。無論如何,問題不在於優化本身,而在於GCC reverse_iterators與惰性迭代器實現不兼容。顯然,優化只是重新安排堆棧內存,以便錯誤變得明顯 – Triskeldeian

0

經過一輪非常有利可圖的討論之後,Revolver_Ocelot的答案指出我要進一步研究reverse_iterators的實現。據來自他的標準報價:

24.5.1.3.4符* [reverse.iter.op。星]

reference operator*() const;

1種效果:

deref_tmp = current; 
--deref_tmp; 
return *deref_tmp; 

2 [注:該操作必須使用的輔助構件變量而不是一個臨時變量,以避免 返回該持續超過壽命的參考它的 關聯的迭代器。 (參見24.2。)-end說明】

尋找標準庫的標題stl_iterator.c的內部,由GCC 4.9 Debian中8實施:

/** 
    * @return A reference to the value at @c --current 
    * 
    * This requires that @c --current is dereferenceable. 
    * 
    * @warning This implementation requires that for an iterator of the 
    *   underlying iterator type, @c x, a reference obtained by 
    *   @c *x remains valid after @c x has been modified or 
    *   destroyed. This is a bug: http://gcc.gnu.org/PR51823 
    */ 
    reference 
    operator*() const 
    { 
_Iterator __tmp = current; 
return *--__tmp; 
    } 

通知警告:

警告: 該實現要求,對於 的底層迭代器類型,@cx,參考獲得的b一個迭代y @c * x在@c x被修改或 銷燬後仍然有效。這是一個錯誤:http://gcc.gnu.org/PR51823