Zachowanie C ++ 20 łamiące istniejący kod za pomocą operatora równości?

Jan 10 2021

Natknąłem się na to podczas debugowania tego pytania .

Zmniejszyłem to do samego końca, używając operatorów Boost :

  1. 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.

  2. 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

81 Barry Jan 10 2021 at 07:27

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 == ysama w sobie ma tylko jednego kandydata: element kandydata ( #1), który obejmuje niejawną konwersję ydo 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 ( #1odwrócone). #2jest lepszym dopasowaniem - dokładnie dopasowujemy oba argumenty zamiast wywoływać konwersję, więc jest ona zaznaczona.

Teraz jednak x == yma teraz dwóch kandydatów: kandydata na członka ponownie ( #1), ale także odwróconego kandydata niebędącego członkiem ( #2odwrócone). #2jest ponownie lepszym dopasowaniem z tego samego powodu, dla którego było lepsze dopasowanie wcześniej: żadne konwersje nie są konieczne. Więc y == xzamiast 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 == yw 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;
};