Comportamento C ++ 20 quebrando código existente com operador de igualdade?

Jan 10 2021

Corri para isso ao depurar esta questão .

Eu reduzi tudo para usar apenas operadores de reforço :

  1. Explorador de compiladores 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 compila e funciona bem com C ++ 17 (habilitado para ubsan / asan) tanto no GCC quanto no Clang.

  2. Quando você altera o construtor implícito para explicit, as linhas problemáticas obviamente não compilam mais em C ++ 17

Surpreendentemente, ambas as versões compilam em C ++ 20 ( v1 e v2 ) , mas levam à recursão infinita (falha ou loop fechado, dependendo do nível de otimização) nas duas linhas que não compilariam em C ++ 17.

Obviamente, esse tipo de bug silencioso surgindo com a atualização para o C ++ 20 é preocupante.

Questões:

  • Este comportamento do c ++ 20 está em conformidade (espero que sim)
  • O que exatamente está interferindo? Eu suspeito que pode ser devido ao novo suporte a "operador de nave espacial" do c ++ 20, mas não entendo como ele muda o comportamento deste código.

Respostas

81 Barry Jan 10 2021 at 07:27

Na verdade, C ++ 20 infelizmente torna esse código infinitamente recursivo.

Aqui está um exemplo reduzido:

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

Vamos apenas dar uma olhada 42 == F{42}.

Em C ++ 17, tínhamos apenas um candidato: o candidato não membro ( #2), então o selecionamos. Seu próprio corpo x == ytem apenas um candidato: o candidato a membro ( #1), que envolve a conversão implícita yem um F. E então esse candidato a membro compara os dois membros inteiros e está tudo bem.

Em C ++ 20, a expressão inicial 42 == F{42}agora tem dois candidatos: o candidato a não membro ( #2) como antes e agora também o candidato a membro #1invertido ( invertido). #2é a melhor correspondência - nós combinamos exatamente os dois argumentos em vez de invocar uma conversão, então ela é selecionada.

Agora, porém, x == yagora tem dois candidatos: o candidato membro novamente ( #1), mas também o candidato não membro #2invertido ( invertido). #2é a melhor correspondência novamente pelo mesmo motivo que era uma correspondência melhor antes: nenhuma conversão necessária. Portanto, avaliamos em y == xvez disso. Recursão infinita.

Candidatos não revertidos são preferidos aos candidatos revertidos, mas apenas como critério de desempate. A melhor sequência de conversão está sempre em primeiro lugar.


Ok, ótimo, como podemos consertar? A opção mais simples é remover totalmente o candidato não 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}aqui avalia como F{42}.operator==(42), o que funciona bem.

Se quisermos manter o candidato não membro, podemos adicionar seu candidato invertido explicitamente:

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

Isso faz com que 42 == F{42}ainda escolha o candidato não membro, mas agora x == yno órgão haverá preferência pelo candidato membro, que então faz a igualdade normal.

Esta última versão também pode remover o candidato não membro. O seguinte também funciona sem recursão para todos os casos de teste (e é como eu escreveria comparações em C ++ 20 daqui para frente):

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