Comportamento in C ++ 20 che rompe il codice esistente con l'operatore di uguaglianza?

Jan 10 2021

Mi sono imbattuto in questo durante il debug di questa domanda .

L'ho ridotto fino a utilizzare solo gli operatori di potenziamento :

  1. Esplora compilatore 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
    }
    

    Questo programma si compila e funziona bene con C ++ 17 (abilitato per ubsan / asan) sia in GCC che in Clang.

  2. Quando si cambia il costruttore implicito in explicit, le linee problematiche ovviamente non vengono più compilate su C ++ 17

Sorprendentemente entrambe le versioni si compilano su C ++ 20 ( v1 e v2 ) , ma portano a ricorsione infinita (crash o tight loop, a seconda del livello di ottimizzazione) sulle due righe che non si compilano su C ++ 17.

Ovviamente questo tipo di bug silenzioso che si insinua con l'aggiornamento a C ++ 20 è preoccupante.

Domande:

  • Questo comportamento di c ++ 20 è conforme (mi aspetto di sì)
  • Cosa sta interferendo esattamente? Ho il sospetto che potrebbe essere dovuto al nuovo supporto "operatore di astronave" di c ++ 20, ma non capisco come cambia il comportamento di questo codice.

Risposte

81 Barry Jan 10 2021 at 07:27

In effetti, C ++ 20 purtroppo rende questo codice infinitamente ricorsivo.

Ecco un esempio ridotto:

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;
};

Diamo solo un'occhiata 42 == F{42}.

In C ++ 17, avevamo un solo candidato: il candidato non membro ( #2), quindi lo selezioniamo. Il suo corpo, a sua volta, x == yha un solo candidato: il member candidate ( #1) che implica la conversione implicita yin un file F. E poi quel membro candidato confronta i due membri interi e questo va benissimo.

In C ++ 20, l'espressione iniziale 42 == F{42}ora ha due candidati: entrambi il candidato non membro ( #2) come prima e ora anche il candidato membro #1invertito ( invertito). #2è la corrispondenza migliore: abbiniamo esattamente entrambi gli argomenti invece di invocare una conversione, quindi è selezionata.

Ora, tuttavia, x == yora ha due candidati: il candidato membro di nuovo ( #1), ma anche il candidato non membro #2invertito ( invertito). #2è di nuovo la corrispondenza migliore per lo stesso motivo per cui prima era una corrispondenza migliore: non sono necessarie conversioni. Quindi valutiamo y == xinvece. Ricorsione infinita.

I candidati non invertiti sono preferiti ai candidati invertiti, ma solo come spareggio. Una migliore sequenza di conversione è sempre la prima.


Va bene, come possiamo risolverlo? L'opzione più semplice è rimuovere completamente il candidato non membro:

struct F {
    /*implicit*/ F(int t_) : t(t_) {}

    bool operator==(F const& o) const { return t == o.t; }

private:
    int t;
};

42 == F{42}qui valuta come F{42}.operator==(42), che funziona bene.

Se vogliamo mantenere il candidato non membro, possiamo aggiungere esplicitamente il suo candidato invertito:

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;
};

Questo fa 42 == F{42}scegliere ancora il candidato non membro, ma ora x == ynel corpo ci sarà il candidato membro che preferisce, che poi fa la normale parità.

Quest'ultima versione può anche rimuovere il candidato non membro. Quanto segue funziona anche senza ricorsione per tutti i casi di test (ed è il modo in cui scriverei i confronti in C ++ 20 in futuro):

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;
};