2016-03-03 138 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