C ++ 20-Verhalten, das vorhandenen Code mit Gleichheitsoperator bricht?
Ich bin darauf gestoßen, als ich diese Frage debuggt habe .
Ich habe es bis auf die Verwendung von Boost-Operatoren reduziert :
Compiler Explorer 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 }
Dieses Programm wird mit C ++ 17 (ubsan / asan aktiviert) sowohl in GCC als auch in Clang kompiliert und funktioniert einwandfrei.
Wenn Sie den impliziten Konstruktor in ändern
explicit
, werden die problematischen Zeilen unter C ++ 17 offensichtlich nicht mehr kompiliert
Überraschenderweise werden beide Versionen unter C ++ 20 ( v1 und v2 ) kompiliert , aber sie führen zu einer unendlichen Rekursion (Absturz oder enge Schleife, abhängig von der Optimierungsstufe) auf den beiden Zeilen, die unter C ++ 17 nicht kompiliert werden könnten.
Offensichtlich ist diese Art von stillem Fehler, der sich durch ein Upgrade auf C ++ 20 einschleicht, besorgniserregend.
Fragen:
- Ist dieses C ++ 20-Verhalten konform (ich erwarte es)
- Was genau stört? Ich vermute, dass dies an der neuen Unterstützung von c ++ 20 für "Raumschiffoperatoren" liegt, verstehe aber nicht, wie sich das Verhalten dieses Codes ändert.
Antworten
In der Tat macht C ++ 20 diesen Code leider unendlich rekursiv.
Hier ist ein reduziertes Beispiel:
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;
};
Schauen wir uns das an 42 == F{42}
.
In C ++ 17 hatten wir nur einen Kandidaten: den Nichtmitgliedskandidaten ( #2
), also wählen wir diesen aus. Sein Körper x == y
selbst hat nur einen Kandidaten: den Mitgliedskandidaten ( #1
), bei dem implizit y
in einen konvertiert wird F
. Und dann vergleicht dieser Mitgliedskandidat die beiden ganzzahligen Mitglieder, und das ist völlig in Ordnung.
In C ++ 20 hat der Anfangsausdruck 42 == F{42}
jetzt zwei Kandidaten: sowohl den Nichtmitgliedskandidaten ( #2
) wie zuvor als auch den umgekehrten Mitgliedskandidaten ( #1
umgekehrt). #2
ist die bessere Übereinstimmung - wir stimmen genau mit beiden Argumenten überein, anstatt eine Konvertierung aufzurufen, daher wird sie ausgewählt.
Jetzt hat jedoch x == y
jetzt zwei Kandidaten: der Mitgliedskandidat wieder ( #1
), aber auch der umgekehrte Nichtmitgliedskandidat ( #2
umgekehrt). #2
ist wieder die bessere Übereinstimmung aus dem gleichen Grund, aus dem es zuvor eine bessere Übereinstimmung war: Es sind keine Konvertierungen erforderlich. Also bewerten wir y == x
stattdessen. Unendliche Rekursion.
Nicht rückgängig gemachte Kandidaten werden gegenüber rückgängig gemachten Kandidaten bevorzugt, jedoch nur als Tiebreaker. Eine bessere Konvertierungssequenz steht immer an erster Stelle.
Okay, großartig, wie können wir das beheben? Die einfachste Möglichkeit besteht darin, den Nichtmitgliedskandidaten vollständig zu entfernen:
struct F {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
private:
int t;
};
42 == F{42}
hier bewertet als F{42}.operator==(42)
, was gut funktioniert.
Wenn wir den Nichtmitgliedskandidaten behalten möchten, können wir seinen umgekehrten Kandidaten explizit hinzufügen:
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;
};
Dies macht 42 == F{42}
immer noch die Wahl des Nichtmitgliedskandidaten, aber jetzt x == y
im Körper wird es den Mitgliedskandidaten bevorzugen, der dann die normale Gleichheit tut.
Diese letzte Version kann auch den Nichtmitgliedskandidaten entfernen. Das Folgende funktioniert auch ohne Rekursion für alle Testfälle (und so würde ich in Zukunft Vergleiche in C ++ 20 schreiben):
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;
};