Comportement C ++ 20 rompant le code existant avec l'opérateur d'égalité?
Je suis tombé sur cela lors du débogage de cette question .
Je l'ai réduit complètement pour n'utiliser que les opérateurs Boost :
Explorateur de compilateur C ++ 17 C ++ 20
#include <boost/operators.hpp> struct F : boost::totally_ordered1<F, boost::totally_ordered2<F, int>> { /*implicit*/ F(int t_) : t(t_) {} bool operator==(F const& o) const { return t == o.t; } bool operator< (F const& o) const { return t < o.t; } private: int t; }; int main() { #pragma GCC diagnostic ignored "-Wunused" F { 42 } == F{ 42 }; // OKAY 42 == F{42}; // C++17 OK, C++20 infinite recursion F { 42 } == 42; // C++17 OK, C++20 infinite recursion }
Ce programme se compile et fonctionne correctement avec C ++ 17 (ubsan / asan activé) à la fois dans GCC et Clang.
Lorsque vous changez le constructeur implicite en
explicit
, les lignes problématiques ne se compilent évidemment plus sur C ++ 17
Étonnamment, les deux versions se compilent sur C ++ 20 ( v1 et v2 ) , mais elles conduisent à une récursivité infinie (crash ou boucle serrée, selon le niveau d'optimisation) sur les deux lignes qui ne compileraient pas sur C ++ 17.
De toute évidence, ce genre de bogue silencieux qui s'installe lors de la mise à niveau vers C ++ 20 est inquiétant.
Des questions:
- Est-ce que ce comportement C ++ 20 est conforme (je pense que oui)
- Qu'est-ce qui interfère exactement? Je soupçonne que cela pourrait être dû au nouveau support des "opérateurs de vaisseau spatial" de C ++ 20, mais je ne comprends pas comment cela change le comportement de ce code.
Réponses
En effet, C ++ 20 rend malheureusement ce code infiniment récursif.
Voici un exemple réduit:
struct F {
/*implicit*/ F(int t_) : t(t_) {}
// member: #1
bool operator==(F const& o) const { return t == o.t; }
// non-member: #2
friend bool operator==(const int& y, const F& x) { return x == y; }
private:
int t;
};
Regardons juste 42 == F{42}
.
En C ++ 17, nous n'avions qu'un seul candidat: le candidat non membre ( #2
), nous le sélectionnons donc. Son corps, x == y
lui-même, n'a qu'un seul candidat: le membre candidat ( #1
) qui consiste implicitement à se convertir y
en un F
. Et puis ce membre candidat compare les deux membres entiers et c'est tout à fait correct.
En C ++ 20, l'expression initiale 42 == F{42}
a maintenant deux candidats: à la fois le candidat non-membre ( #2
) comme auparavant et maintenant aussi le candidat membre #1
inversé ( inversé). #2
est la meilleure correspondance - nous correspondons exactement aux deux arguments au lieu d'appeler une conversion, elle est donc sélectionnée.
Maintenant, cependant, a x == y
maintenant deux candidats: le candidat membre à nouveau ( #1
), mais aussi le candidat non-membre #2
inversé ( inversé). #2
est à nouveau la meilleure correspondance pour la même raison que c'était une meilleure correspondance avant: aucune conversion nécessaire. Nous évaluons donc à la y == x
place. Récursivité infinie.
Les candidats non inversés sont préférés aux candidats inversés, mais uniquement en cas d'égalité. Une meilleure séquence de conversion est toujours la première.
Très bien, comment pouvons-nous résoudre ce problème? L'option la plus simple consiste à supprimer complètement le candidat non membre:
struct F {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
private:
int t;
};
42 == F{42}
ici évalue comme F{42}.operator==(42)
, ce qui fonctionne très bien.
Si nous voulons conserver le candidat non membre, nous pouvons ajouter explicitement son candidat inversé:
struct F {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
bool operator==(int i) const { return t == i; }
friend bool operator==(const int& y, const F& x) { return x == y; }
private:
int t;
};
Cela fait 42 == F{42}
toujours choisir le candidat non-membre, mais maintenant x == y
dans le corps, il va préférer le candidat membre, qui fait alors l'égalité normale.
Cette dernière version peut également supprimer le candidat non membre. Ce qui suit fonctionne également sans récursivité pour tous les cas de test (et c'est ainsi que j'écrirais des comparaisons en C ++ 20 à l'avenir):
struct F {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
bool operator==(int i) const { return t == i; }
private:
int t;
};