web-dev-qa-db-ja.com

逆イテレータは、最適化されるとガベージを返します

数値のような型をとるAsIteratorテンプレートクラスがあります。この例では、intだけで、それをイテレータ(++および--インクリメント)に変換します。数値をデクリメントすると、operator*はその数値への参照を返すだけです)。

これは正常に機能します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;
}

これはおそらく、挿入された最大の数値から最小の数値にループダウンして、この実行のようにそれらを出力する必要があります(-O0でコンパイル):

Insert two integers: 1 4 
3
2
1

代わりに、-Oで得られるものは次のとおりです。

Insert two integers: 1 4 
1
0
0

あなたができる ここでオンラインで試してみてください ;数値は異なる場合がありますが、バイナリを最適化する場合は常に「間違っています」。


私が試したこと:

  • 入力整数をハードコーディングするだけで、同じ結果が得られます。
  • この問題は、gcc 5.4.およびclang 3.8.でも発生します。これは、libc ++を使用している場合も同様です。
  • すべてのオブジェクトをconstにする(つまり、const int &を返し、すべての変数をそのように宣言する)と、それは修正されません。
  • たとえば、一部のreverse_iteratorで同じようにstd::vector<int>を使用すると、正常に機能します。
  • 通常の順方向または逆方向ループにAsIterator<int>を使用するだけで、正常に機能します。
  • 私のテストでは、出力される定数0は実際にはコンパイラによってハードコードであり、-S -Oでコンパイルすると、printfの呼び出しはすべて次のようになります。
    movl    $.L.str.2, %edi  # .L.str.2 is "%d\n"
    xorl    %eax, %eax
    callq   printf

ここでのclanggccの動作の一貫性を考えると、彼らはそれを正しく行っていると確信しており、誤解しましたが、実際にはそれを見ることができません。

42
Spak

std::reverse_iteratorlibstdc ++実装を見ると、興味深いことがわかります。

  /**
   *  @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;
  }

@warningセクションは、基礎となるイテレータータイプの要件は、基礎となるイテレーターが変更/破棄された後でも*xが有効なままでなければならないことを示しています。

前述のバグリンク を見ると、より興味深い情報が明らかになります。

c ++ 03とC++ 11の間のある時点で、reverse_iterator :: operator *の定義が変更され、これが明確になり、libstdc ++の実装が正しくなくなりました。標準は今言う:

[注:この操作では、関連するイテレーターの存続期間を超えて存続する参照が返されないように、一時変数ではなく補助メンバー変数を使用する必要があります。 (24.2を参照)—エンドノート]

jonathan Wakelyによるコメント(2012)

だからそれはバグのように見えます...しかしトピックの終わりに:

Reverse_iteratorの定義は、追加のメンバーを使用しないC++ 03バージョンに戻されたため、「スタッシングイテレーター」をreverse_iteratorと一緒に使用することはできません。

jonathan Wakelyによるコメント(2014)

したがって、std::reverse_iteratorを「スタッシングイテレータ」とともに使用すると、実際にUBにつながるようです。


DR 2204: "reverse_iteratorはベースイテレータの2番目のコピーを必要としないはずです" を見るとさらに明確になります問題:

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です。

45
Vittorio Romeo