Подтвердить что ты не робот

Обратный итератор возвращает мусор при оптимизации

У меня есть класс шаблона 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.0 и clang 3.8.0, также при использовании libС++;
  • создание всех объектов 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

Учитывая последовательность поведения clang и gcc, я уверен, что они делают это правильно, и я неправильно понял, но я действительно не вижу этого.

4b9b3361

Ответ 1

Глядя на std::reverse_iterator реализация libstdС++ показывает что-то интересное:

  /**
   *  @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 должно оставаться в силе даже после того, как базовый итератор будет изменен/уничтожен.

Глядя на упоминаемую ссылку об ошибке, вы найдете более интересную информацию:

в какой-то момент между С++ 03 и С++ 11 было изменено определение reverse_iterator:: operator *, чтобы уточнить это, что делает реализацию libstdС++ неправильной. В стандарте теперь говорится:

     

[Примечание. Эта операция должна использовать вспомогательную переменную-член, а не временную переменную, чтобы избежать возврата ссылки, которая сохраняется за пределами жизни связанного с ней итератора. (См. 24.2.) -end note]

комментарий Джонатана Вакли (2012)

Итак, это похоже на ошибку... но в конце темы:

Определение reverse_iterator было возвращено к версии С++ 03, в которой не используется дополнительный элемент, поэтому "стоп-итераторы" не могут использоваться с reverse_iterator.

комментарий Джонатана Вакли (2014)

Таким образом, кажется, что использование std::reverse_iterator с "stashing iterators" действительно приводит к UB.


Глядя на DR 2204: " reverse_iterator, не требуется вторая копия базового итератора, далее проясняет проблему:

Это примечание в 24.5.1.3.4 [reverse.iter.op.star]/2:

[Примечание. Эта операция должна использовать вспомогательную переменную-член, а не временную переменную, чтобы избежать возврата ссылки, которая сохраняется за пределами жизни связанного с ней итератора. (См. 24.2.) -end note]

     

[мое примечание: я думаю, что вышеупомянутое примечание исправит вашу проблему с UB]

неверно, поскольку такие реализации итератора исключаются 24.2.5 [forward.iterators]/6, где говорится:

Если a и b являются разыменованными, тогда a == b тогда и только тогда, когда * a и * b связаны с одним и тем же объектом.