L’iterator inversé retourne les ordures lorsqu’il est optimisé

J’ai une classe de modèle AsIterator qui prend un type de type numérique, dans cet exemple, juste un int et le convertit en un iterator ( ++ et -- incrémente et décrémente le nombre, et operator* renvoie simplement une référence).

Cela fonctionne bien sauf si elle est enveloppée dans un std::reverse_iterator et compilée avec une optimisation quelconque ( -O est suffisant). Lorsque j’optimise le binary, le compilateur supprime l’appel de déréférencement à reverse_iterator et le remplace par une valeur étrange. Il faut noter qu’il fait toujours le bon nombre d’itérations . C’est juste la valeur obtenue par un iterator inversé qui est une erreur.

Considérez le code suivant:

 #include  #include  template class AsIterator : public std::iterator { 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> 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 real_begin(a); AsIterator real_end(b); for (ReverseIt rev_it(real_end); rev_it != ReverseIt(real_begin); ++rev_it) { printf("%d\n", *rev_it); } return 0; } 

Cela devrait supposer de passer du plus grand nombre inséré au plus bas et de les imprimer, comme dans cette exécution (compilé avec -O0 ):

 Insert two integers: 1 4 3 2 1 

Ce que je reçois avec -O est à la place:

 Insert two integers: 1 4 1 0 0 

Vous pouvez l’ essayer en ligne ici ; les nombres peuvent varier mais ils sont toujours “erronés” lors de l’optimisation du binary.


Ce que j’ai essayé:

  • coder en dur les entiers en entrée suffit pour produire le même résultat;
  • le problème persiste avec gcc 5.4.0 et clang 3.8.0 , également avec libc ++ ;
  • rendre tous les objects const (c’est-à-dire renvoyer const int & , et déclarer toutes les variables en tant que telles) ne le répare pas;
  • utiliser l’ reverse_iterator de la même manière, par exemple, std::vector fonctionne correctement;
  • si j’utilise simplement AsIterator pour une boucle normale avant ou arrière, cela fonctionne bien.
  • dans mes tests, la constante 0 qui est imprimée est réellement codée en dur par le compilateur, les appels à printf ressemblent tous à ceci quand ils sont compilés avec -S -O :
  movl $.L.str.2, %edi # .L.str.2 is "%d\n" xorl %eax, %eax callq printf 

Étant donné la cohérence du comportement de clang et de gcc , je suis presque sûr qu’ils le font bien et que j’ai mal compris, mais je ne le vois vraiment pas.

Regarder l’ std::reverse_iterator de libstdc ++ de std::reverse_iterator révèle quelque chose d’intéressant:

  /** * @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, @cx, a reference obtained by * @c *x remains valid after @cx has been modified or * destroyed. This is a bug: http://gcc.gnu.org/PR51823 */ _GLIBCXX17_CONSTEXPR reference operator*() const { _Iterator __tmp = current; return *--__tmp; } 

La section @warning indique qu’une condition du type d’iterator sous-jacent est que *x doit restr valide même après la modification / destruction de l’iterator sous-jacent.

En regardant le lien de bogue mentionné, on découvre des informations plus intéressantes:

à un moment donné entre C ++ 03 et C ++ 11, la définition de reverse_iterator :: operator * a été modifiée pour clarifier cela, rendant l’implémentation de libstdc ++ incorrecte. La norme dit maintenant:

[Remarque: cette opération doit utiliser une variable membre auxiliaire plutôt qu’une variable temporaire pour éviter de renvoyer une référence qui persiste au-delà de la durée de vie de l’iterator associé. (Voir 24.2.) – append une note]

commentaire de Jonathan Wakely (2012)

Donc, cela ressemble à un bug … mais à la fin du sujet:

La définition de reverse_iterator est revenue à la version C ++ 03, qui n’utilise pas de membre supplémentaire, de sorte que les “iterators itinérants” ne peuvent pas être utilisés avec reverse_iterator.

commentaire de Jonathan Wakely (2014)

Il semble donc que l’utilisation de std::reverse_iterator avec des “iterators” cache bien UB.


En regardant le DR 2204: reverse_iterator ne devrait pas nécessiter une deuxième copie de l’iterator de base” , clarifie davantage le problème:

Cette note dans 24.5.1.3.4 [reverse.iter.op.star] / 2:

[Remarque: cette opération doit utiliser une variable membre auxiliaire plutôt qu’une variable temporaire pour éviter de renvoyer une référence qui persiste au-delà de la durée de vie de l’iterator associé. (Voir 24.2.) – append une note]

[ma note: je pense que la note ci-dessus résoudrait votre problème avec UB]

est incorrect car ces implémentations d’iterators sont exclues par 24.2.5 [forward.iterators] / 6, où il est dit:

Si a et b sont tous deux déréférencables, alors un == b si et seulement si * a et * b sont liés au même object.