Zachowanie C ++ 20 łamiące istniejący kod za pomocą operatora równości?
Natknąłem się na to podczas debugowania tego pytania .
Zmniejszyłem to do samego końca, używając operatorów Boost :
Kompilator 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 }
Ten program kompiluje się i działa dobrze z C ++ 17 (włączone ubsan / asan) zarówno w GCC, jak i Clang.
Gdy zmienisz niejawny konstruktor na
explicit
, problematyczne wiersze oczywiście nie będą już kompilować się w C ++ 17
Zaskakujące jest, że obie wersje kompilują się na C ++ 20 ( v1 i v2 ) , ale prowadzą do nieskończonej rekursji (awaria lub ciasna pętla, w zależności od poziomu optymalizacji) w dwóch wierszach, które nie mogłyby się skompilować na C ++ 17.
Oczywiście ten rodzaj cichego błędu wkradającego się po aktualizacji do C ++ 20 jest niepokojące.
Pytania:
- Czy to zachowanie jest zgodne z c ++ 20 (spodziewam się)
- Co dokładnie przeszkadza? Podejrzewam, że może to być spowodowane obsługą nowego „operatora statku kosmicznego” w c ++ 20, ale nie rozumiem, jak zmienia to zachowanie tego kodu.
Odpowiedzi
Rzeczywiście, C ++ 20 niestety sprawia, że ten kod jest nieskończenie rekurencyjny.
Oto skrócony przykład:
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;
};
Spójrzmy tylko na 42 == F{42}
.
W C ++ 17 mieliśmy tylko jednego kandydata: kandydata niebędącego członkiem ( #2
), więc wybieramy to. Jego treść x == y
sama w sobie ma tylko jednego kandydata: element kandydata ( #1
), który obejmuje niejawną konwersję y
do F
. A potem ten kandydat na członka porównuje dwa elementy będące liczbami całkowitymi i to jest w porządku.
W C ++ 20 początkowe wyrażenie 42 == F{42}
ma teraz dwóch kandydatów: zarówno kandydata nie będącego członkiem ( #2
), jak poprzednio, a teraz również kandydata na odwrócony element członkowski ( #1
odwrócone). #2
jest lepszym dopasowaniem - dokładnie dopasowujemy oba argumenty zamiast wywoływać konwersję, więc jest ona zaznaczona.
Teraz jednak x == y
ma teraz dwóch kandydatów: kandydata na członka ponownie ( #1
), ale także odwróconego kandydata niebędącego członkiem ( #2
odwrócone). #2
jest ponownie lepszym dopasowaniem z tego samego powodu, dla którego było lepsze dopasowanie wcześniej: żadne konwersje nie są konieczne. Więc y == x
zamiast tego oceniamy . Nieskończona rekurencja.
Kandydaci nieodwróceni są preferowani, a nie odwróceni kandydaci, ale tylko jako rozstrzygający remis. Lepsza sekwencja konwersji jest zawsze na pierwszym miejscu.
Dobra, jak możemy to naprawić? Najprostszą opcją jest całkowite usunięcie kandydata niebędącego członkiem:
struct F {
/*implicit*/ F(int t_) : t(t_) {}
bool operator==(F const& o) const { return t == o.t; }
private:
int t;
};
42 == F{42}
tutaj ocenia się jako F{42}.operator==(42)
, co działa dobrze.
Jeśli chcemy zachować kandydata niebędącego członkiem, możemy jawnie dodać jego odwróconego kandydata:
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;
};
To sprawia, że 42 == F{42}
nadal wybiera się kandydata niebędącego członkiem, ale teraz x == y
w ciele będzie preferowany kandydat na członka, który wtedy robi normalną równość.
Ta ostatnia wersja może również usunąć kandydata niebędącego członkiem. Poniższe działa również bez rekurencji dla wszystkich przypadków testowych (i tak bym pisał porównania w C ++ 20 w przyszłości):
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;
};