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;

除最后两行外,所有测试的测试结果都符合预期

当优化开启时,测试的最后两行会崩溃。

该系统实际上运行良好,并不比任何其他迭代器更危险。当然,如果容器在他的眼皮底下被删除,它会失败,并且通过复制使用返回的值可能更安全,而不仅仅是保留引用,但这是题外话

4

4 回答 4

2

“即使m_Current成员被标记为可变也已优化”

这告诉我你假设优化器关心mutable. 它没有。constmutable已被早期编译阶段剥离。

如果它们是内联的,那么为什么优化器会删除这两个语句?我怀疑在内联之后,优化器可以证明这两次写入是空操作,要么是因为m_Current变量必须已经持有正确的值,要么是因为后续的使用m_Current使得它没有实际意义。微不足道的以下情况使这些写入成为无操作:

Lazy_Iterator LI = foo(); // Theoretically writes
*LI = bar(); // Overwrites the previous value.
于 2016-03-03T12:28:35.020 回答
2

reverse_iterator持有的物理迭代器(返回的内容)和它指向的逻辑值之间存在差异问题.base():它们是一对一的。reverse_iterator可能会return *(--internal_iterator);在 dereference 上做,这会让你对被破坏的函数局部临时的内部进行悬空引用。

在再次阅读标准后,我发现它有额外的要求来避免这种情况,阅读注释。

我还发现 GCC 4.9 标准库不兼容。它使用临时的。所以,我认为这是一个 GCC 错误。

编辑:标准报价

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

reference operator*() const;

1效果:

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

2 [注意:此操作必须使用辅助成员变量而不是临时变量,以避免返回持续超过其关联迭代器生命周期的引用。(见 24.2。)——尾注]

追读: 图书馆缺陷报告198

而且似乎回到了旧的行为

后期编辑:P0031 在 C++17 工作草案中被投票。它声明reverse_iterator使用临时而不是成员来保存中间值。

于 2016-03-03T12:44:24.147 回答
1

如果您必须发布一个重现该问题的可编译片段(实际上我无法使用 GCC 4.9 重现它),我认为您有未定义的行为并且由 O2 触发(O2 启用了可以破坏未定义行为的优化)。你应该有一个指向

Container<T> 

迭代器内部。

无论如何,请注意惰性迭代器会破坏 std 迭代器的合同,我认为更好的选择是制作惰性值的常规容器,您可以通过这种方式跳过创建自定义容器和迭代器;)(查看代理模式) .

于 2016-03-03T12:27:10.460 回答
0

经过一轮非常有利可图的讨论后,Revolver_Ocelot 的回答让我进一步研究 reverse_iterators 的实现。根据他对标准的引用:

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

reference operator*() const;

1 效果:

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

2 [注意:此操作必须使用辅助成员变量而不是临时变量,以避免返回持续超过其关联迭代器生命周期的引用。(见 24.2。)——尾注]

查看 Debian 8 中由 GCC 4.9 实现的标准库的标头 stl_iterator.c 内部:

  /**
   *  @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 的迭代器,@c *x 获得的引用在@cx 被修改或销毁后仍然有效。这是一个错误: http: //gcc.gnu.org/PR51823

于 2016-03-03T13:07:31.547 回答