反向迭代器在优化时返回垃圾

Pie*_*ica 42 c++ reverse iterator loops compiler-optimization

我有一个AsIterator模板类,它采用类似数字的类型,在这个例子中只是一个int,并将其转换为迭代器(++--递增和递减数字,并operator*只返回对它的引用).

这工作正常,除非它被包装成a std::reverse_iterator并使用任何优化编译(-O足够).当我优化二进制文件时,编译器去除reverse_iterator对它的取消引用调用,并用一些奇怪的值替换它.必须注意的是,它仍然可以进行正确的迭代次数.它只是反向迭代器获得的值,即垃圾.

请考虑以下代码:

#include <iterator>
#include <cstdio>

template<typename T>
class AsIterator : public std::iterator<std::bidirectional_iterator_tag, T> {
    T v;
public:
    AsIterator(const T & init) : v(init) {}

    T &operator*() { return v; }

    AsIterator &operator++() { ++v; return *this; }
    AsIterator operator++(int) { AsIterator copy(*this); ++(*this); return copy; }
    AsIterator &operator--() { --v; return *this; }
    AsIterator operator--(int) { AsIterator copy(*this); --(*this); return copy; }

    bool operator!=(const AsIterator &other) const {return v != other.v;}
    bool operator==(const AsIterator &other) const {return v == other.v;}
};

typedef std::reverse_iterator<AsIterator<int>> ReverseIt;

int main() {
    int a = 0, b = 0;
    printf("Insert two integers: ");
    scanf("%d %d", &a, &b);
    if (b < a) std::swap(a, b);

    AsIterator<int> real_begin(a);
    AsIterator<int> real_end(b);
    for (ReverseIt rev_it(real_end); rev_it != ReverseIt(real_begin); ++rev_it) {
        printf("%d\n", *rev_it);
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这应该假设从最高插入数字循环到最低并打印它们,例如在此运行中(编译-O0):

Insert two integers: 1 4 
3
2
1
Run Code Online (Sandbox Code Playgroud)

我得到的-O是:

Insert two integers: 1 4 
1
0
0
Run Code Online (Sandbox Code Playgroud)

你可以在网上试试 ; 数字可能会有所不同,但在优化二进制文件时它们总是"错误的".


我尝试过的:

  • 对输入整数进行硬编码就足以产生相同的结果;
  • 使用libc ++时,gcc 5.4.0clang 3.8.0仍然存在问题;
  • 制作所有对象const(即返回const int &,并声明所有变量)并不能解决问题;
  • reverse_iterator相同的方式使用例如一些std::vector<int>工作正常;
  • 如果我只是AsIterator<int>用于正常的前向或后向循环,它可以正常工作.
  • 在我的测试中,0打印的常量实际上是由编译器硬编码的,当编译时,对printf所有的调用看起来像这样-S -O:
    movl    $.L.str.2, %edi  # .L.str.2 is "%d\n"
    xorl    %eax, %eax
    callq   printf
Run Code Online (Sandbox Code Playgroud)

考虑到clanggcc在这里的行为的一致性,我很确定他们做得对,我误解了,但我真的看不到它.

Vit*_*meo 45

std::reverse_iterator的libstdc ++实现揭示了一些有趣的事情:

  /**
   *  @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
  */
  _GLIBCXX17_CONSTEXPR reference
  operator*() const
  {
    _Iterator __tmp = current;
     return *--__tmp;
  }
Run Code Online (Sandbox Code Playgroud)

@warning部分告诉我们底层迭代器类型的要求是*x即使在修改/销毁底层迭代器之后也必须保持有效.

查看上述错误链接可以发现更多有趣的信息:

在C++ 03和C++ 11之间的某个时刻,reverse_iterator :: operator*的定义被改为澄清这一点,使得libstdc ++的实现错误.该标准现在说:

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

Jonathan Wakely的评论(2012)

所以它看起来像一个bug ......但在主题的最后:

reverse_iterator的定义已经恢复为C++ 03版本,它不使用额外的成员,因此"存储迭代器"不能与reverse_iterator一起使用.

Jonathan Wakely的评论(2014)

因此,似乎使用std::reverse_iterator"存储迭代器"确实会导致UB.


查看DR 2204:" reverse_iterator不应该要求基础迭代器的第二个副本"进一步澄清了问题:

本说明在24.5.1.3.4 [reverse.iter.op.star]/2中:

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

[我的注意:我认为上述说明会解决您的UB问题]

是不正确的,因为这样的迭代器实现被24.2.5 [forward.iterators]/6排除,其中它表示:

如果a和b都是可解除引用的,则a == b当且仅当*a和*b绑定到同一个对象时.

  • 是的,也与[bug link](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=51823)相关:*"你所写的不是迭代器,它是一个生成器."*- 对于内迭代器返回的任何非持久引用,`reverse_iterator`无法正常工作. (2认同)