¿Comportamiento de C ++ 20 que rompe el código existente con el operador de igualdad?

Jan 10 2021

Me encontré con esto mientras depuraba esta pregunta .

Lo recorté hasta el final para usar solo Operadores de impulso :

  1. Explorador del compilador 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
    }
    

    Este programa se compila y funciona bien con C ++ 17 (ubsan / asan habilitado) tanto en GCC como en Clang.

  2. Cuando cambia el constructor implícito a explicit, las líneas problemáticas obviamente ya no se compilan en C ++ 17

Sorprendentemente, ambas versiones se compilan en C ++ 20 ( v1 y v2 ) , pero conducen a una recursividad infinita (bloqueo o bucle cerrado, según el nivel de optimización) en las dos líneas que no se compilarían en C ++ 17.

Obviamente, este tipo de error silencioso que se infiltra al actualizar a C ++ 20 es preocupante.

Preguntas:

  • ¿Es este comportamiento de c ++ 20 conforme (eso espero)?
  • ¿Qué está interfiriendo exactamente? Sospecho que podría deberse al nuevo soporte de "operador de nave espacial" de c ++ 20, pero no entiendo cómo cambia el comportamiento de este código.

Respuestas

81 Barry Jan 10 2021 at 07:27

De hecho, C ++ 20 desafortunadamente hace que este código sea infinitamente recursivo.

Aquí hay un ejemplo reducido:

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

Echemos un vistazo 42 == F{42}.

En C ++ 17, solo teníamos un candidato: el candidato no miembro ( #2), así que lo seleccionamos. Su cuerpo x == y,, solo tiene un candidato: el miembro candidato ( #1) que implica convertir implícitamente yen un F. Y luego ese miembro candidato compara los dos miembros enteros y esto está totalmente bien.

En C ++ 20, la expresión inicial 42 == F{42}ahora tiene dos candidatos: el candidato no miembro ( #2) como antes y ahora también el candidato miembro #1inverso ( invertido). #2es la mejor coincidencia: coincidimos exactamente con ambos argumentos en lugar de invocar una conversión, por lo que se selecciona.

Ahora, sin embargo, x == yahora tiene dos candidatos: el candidato miembro nuevamente ( #1), pero también el candidato no miembro #2invertido ( invertido). #2es la mejor coincidencia de nuevo por la misma razón que antes era una mejor coincidencia: no se necesitan conversiones. Así que evaluamos en su y == xlugar. Recursión infinita.

Se prefieren los candidatos no revertidos a los candidatos revertidos, pero solo como desempate. Una mejor secuencia de conversión siempre es lo primero.


Está bien, ¿cómo podemos solucionarlo? La opción más simple es eliminar por completo al candidato que no es miembro:

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

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

private:
    int t;
};

42 == F{42}aquí se evalúa como F{42}.operator==(42), que funciona bien.

Si queremos mantener el candidato no miembro, podemos agregar su candidato invertido explícitamente:

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

Esto hace que 42 == F{42}aún se elija al candidato no miembro, pero ahora x == yen el organismo se preferirá al candidato miembro, que luego hace la igualdad normal.

Esta última versión también puede eliminar al candidato no miembro. Lo siguiente también funciona sin recursividad para todos los casos de prueba (y así es como escribiría comparaciones en C ++ 20 en el 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;
};