Quebrar a mudança no C ++ 20 ou a regressão no clang-trunk / gcc-trunk ao sobrecarregar a comparação de igualdade com o valor de retorno não-booleano?

11

O código a seguir é compilado com clang-trunk no modo c ++ 17, mas é interrompido no modo c ++ 2a (próximo c ++ 20):

// Meta struct describing the result of a comparison
struct Meta {};

struct Foo {
    Meta operator==(const Foo&) {return Meta{};}
    Meta operator!=(const Foo&) {return Meta{};}
};

int main()
{
    Meta res = (Foo{} != Foo{});
}

Também compila bem com gcc-trunk ou clang-9.0.0: https://godbolt.org/z/8GGT78

O erro com clang-trunk e -std=c++2a:

<source>:12:19: error: use of overloaded operator '!=' is ambiguous (with operand types 'Foo' and 'Foo')
    Meta res = (f != g);
                ~ ^  ~
<source>:6:10: note: candidate function
    Meta operator!=(const Foo&) {return Meta{};}
         ^
<source>:5:10: note: candidate function
    Meta operator==(const Foo&) {return Meta{};}
         ^
<source>:5:10: note: candidate function (with reversed parameter order)

Eu entendo que o C ++ 20 tornará possível apenas sobrecarregar operator==e o compilador será gerado automaticamente operator!=negando o resultado de operator==. Tanto quanto eu entendo, isso só funciona enquanto o tipo de retorno é bool.

A fonte do problema é que em Eigen que declarar um conjunto de operadores ==, !=, <, ... entre Arrayobjectos ou Arraye escalares, que retornam (uma expressão de) uma matriz de bool(a qual pode então ser acedida elemento a elemento, ou de outro modo utilizados ) Por exemplo,

#include <Eigen/Core>
int main()
{
  Eigen::ArrayXd a(10);
  a.setRandom();
  return (a != 0.0).any();
}

Em contraste com o meu exemplo acima, isso ainda falha com o gcc-trunk: https://godbolt.org/z/RWktKs . Ainda não consegui reduzir isso a um exemplo que não é Eigen, que falha nos clang-trunk e no gcc-trunk (o exemplo na parte superior é bastante simplificado).

Relatório de problema relacionado: https://gitlab.com/libeigen/eigen/issues/1833

Minha pergunta real: isso é realmente uma mudança de quebra no C ++ 20 (e existe a possibilidade de sobrecarregar os operadores de comparação para retornar Meta-objetos) ou é mais provável uma regressão no clang / gcc?

chtz
fonte

Respostas:

5

O problema de Eigen parece reduzir ao seguinte:

using Scalar = double;

template<class Derived>
struct Base {
    friend inline int operator==(const Scalar&, const Derived&) { return 1; }
    int operator!=(const Scalar&) const;
};

struct X : Base<X> {};

int main() {
    X{} != 0.0;
}

Os dois candidatos à expressão são

  1. o candidato reescrito de operator==(const Scalar&, const Derived&)
  2. Base<X>::operator!=(const Scalar&) const

Por [over.match.funcs] / 4 , como operator!=não foi importado para o escopo Xpor uma declaração de uso , o tipo do parâmetro implícito do objeto para # 2 é const Base<X>&. Como resultado, o número 1 tem uma sequência de conversão implícita melhor para esse argumento (correspondência exata, em vez da conversão de derivada para base). A seleção de # 1 torna o programa mal formado.

Correções possíveis:

  • Adicionar using Base::operator!=;a Derivedou
  • Altere operator==para tirar um em const Base&vez de a const Derived&.
TC
fonte
Existe uma razão pela qual o código real não pôde retornar um booldeles operator==? Porque essa parece ser a única razão pela qual o código está mal formado sob as novas regras.
Nicol Bolas
4
O código real envolve um operator==(Array, Scalar)que faz comparação entre elementos e retorna um Arrayde bool. Você não pode transformar isso em um boolsem quebrar todo o resto.
TC
2
Isso parece um defeito no padrão. As regras para reescrever operator==não deveriam afetar o código existente, no entanto, nesse caso, porque a verificação de um boolvalor de retorno não faz parte da seleção de candidatos para reescrita.
Nicol Bolas 8/03
2
@ NicolBolas: O princípio geral a ser seguido é que a verificação é para saber se você pode fazer algo ( por exemplo , chamar o operador), não se deve , para evitar que alterações na implementação afetem silenciosamente a interpretação de outro código. Acontece que as comparações reescritas quebram muitas coisas, mas principalmente as que já eram questionáveis ​​e fáceis de corrigir. Portanto, para o bem ou para o mal, essas regras foram adotadas de qualquer maneira.
Davis Herring
Uau, muito obrigado, acho que sua solução resolverá o nosso problema (não tenho tempo para instalar o tronco gcc / clang com um esforço razoável no momento, por isso vou verificar se isso quebra alguma coisa nas últimas versões estáveis ​​do compilador )
chtz
11

Sim, o código de fato quebra no C ++ 20.

A expressão Foo{} != Foo{}possui três candidatos no C ++ 20 (considerando que havia apenas um no C ++ 17):

Meta operator!=(Foo& /*this*/, const Foo&); // #1
Meta operator==(Foo& /*this*/, const Foo&); // #2
Meta operator==(const Foo&, Foo& /*this*/); // #3 - which is #2 reversed

Isso vem das novas regras de candidato reescritas em [over.match.oper] /3.4 . Todos esses candidatos são viáveis, já que nossos Fooargumentos não são const. Para encontrar o melhor candidato viável, precisamos passar por nossos desempates.

As regras relevantes para a melhor função viável são, de [over.match.best] / 2 :

Dadas essas definições, uma função viável F1é definida como uma função melhor do que outra função viável F2se, para todos os argumentos i, não for uma sequência de conversão pior que e, em seguida, ICSi(F1)ICSi(F2)

  • [... muitos casos irrelevantes para este exemplo ...] ou, se não for isso, então
  • F2 é um candidato reescrito ([over.match.oper]) e F1 não é
  • F1 e F2 são candidatos reescritos, e F2 é um candidato sintetizado com ordem inversa de parâmetros e F1 não é

#2e #3são candidatos reescritos e #3inverte a ordem dos parâmetros, enquanto #1não é reescrito. Mas, para chegar a esse desempatador, precisamos primeiro passar pela condição inicial: para todos os argumentos, as seqüências de conversão não são piores.

#1é melhor do que #2porque todas as seqüências de conversão são iguais (trivialmente, porque os parâmetros de função são os mesmos) e #2é um candidato reescrito enquanto #1não é.

Mas ... ambos os pares #1/ #3e #2/ #3 ficam presos nessa primeira condição. Nos dois casos, o primeiro parâmetro tem uma melhor sequência de conversão para #1/ #2enquanto o segundo parâmetro tem uma melhor sequência de conversão para #3(o parâmetro constprecisa passar por uma constqualificação extra , para ter uma pior sequência de conversão). Este constflip-flop faz com que não possamos preferir nenhum deles.

Como resultado, toda a resolução de sobrecarga é ambígua.

Tanto quanto eu entendo, isso só funciona enquanto o tipo de retorno é bool.

Isso não está correto. Consideramos incondicionalmente candidatos reescritos e revertidos. A regra que temos é, de [over.match.oper] / 9 :

Se um operator==candidato reescrito for selecionado por resolução de sobrecarga para um operador @, seu tipo de retorno será cv bool

Ou seja, ainda consideramos esses candidatos. Mas se o melhor candidato viável é operator==aquele que retorna, digamos, Meta- o resultado é basicamente o mesmo que se esse candidato tivesse sido excluído.

Nós fez não quer estar em um estado onde a resolução de sobrecarga teria que considerar o tipo de retorno. E, de qualquer forma, o fato de o código retornar aqui Metaé irrelevante - o problema também existiria se ele retornasse bool.


Felizmente, a correção aqui é fácil:

struct Foo {
    Meta operator==(const Foo&) const;
    Meta operator!=(const Foo&) const;
    //                         ^^^^^^
};

Depois de criar os dois operadores de comparação const, não há mais ambiguidade. Todos os parâmetros são iguais, portanto, todas as seqüências de conversão são trivialmente iguais. #1agora venceria #3por não ser reescrita e #2agora venceria #3por não ser revertida - o que torna #1o melhor candidato viável. O mesmo resultado que tivemos no C ++ 17, apenas mais algumas etapas para chegar lá.

Barry
fonte
Não queríamos estar em um estado em que a resolução de sobrecarga teria que considerar o tipo de retorno. ” Apenas para ficar claro, enquanto a resolução de sobrecarga em si não considera o tipo de retorno, as operações reescritas subsequentes o fazem . O código de alguém está mal formado se a resolução de sobrecarga selecionar uma reescrita ==e o tipo de retorno da função selecionada não bool. Mas esse abate não ocorre durante a própria resolução de sobrecarga.
Nicol Bolas 6/03
Na verdade, ele é apenas mal formado se o tipo de retorno é algo que não suporta operador! ...
Chris Dodd
11
@ChrisDodd Não, precisa ser exatamente cv bool(e antes dessa alteração, o requisito era de conversão contextual para bool- ainda não !)
Barry
Infelizmente, isso não resolve o meu problema real, mas foi porque não consegui fornecer um MRE que realmente descrevesse o meu problema. Aceito isso e, quando puder reduzir meu problema corretamente, farei uma nova pergunta ...
chtz 07/03
2
Parece que uma redução adequada para a edição original é gcc.godbolt.org/z/tFy4qz
TC
5

[over.match.best] / 2 lista como as sobrecargas válidas em um conjunto são priorizadas. A Seção 2.8 nos diz que F1é melhor do que F2se (entre muitas outras coisas):

F2é um candidato reescrito ([over.match.oper]) e F1não é

O exemplo lá mostra um explícito operator<sendo chamado mesmo que operator<=>esteja lá.

E [over.match.oper] /3.4.3 nos diz que a candidatura operator==nessas circunstâncias é um candidato reescrito.

No entanto , seus operadores esquecem uma coisa crucial: eles devem ser constfunções. E fazê-los não constfaz com que aspectos anteriores da resolução de sobrecarga entrem em cena. Nem função é uma correspondência exata, como não constPara- constconversões precisam acontecer para diferentes argumentos. Isso causa a ambiguidade em questão.

Depois de criá -los const, o Clang tronco é compilado .

Não posso falar com o resto de Eigen, pois não conheço o código, é muito grande e, portanto, não pode caber em um MCVE.

Nicol Bolas
fonte
2
Só chegamos ao desempate que você listou se houver igualmente boas conversões para todos os argumentos. Mas não há: devido à falta const, os candidatos não revertidos têm uma melhor sequência de conversão para o segundo argumento e o candidato revertido tem uma melhor sequência de conversão para o primeiro argumento.
Richard Smith
@RichardSmith: Sim, esse era o tipo de complexidade que eu estava falando. Mas eu não queria ter que realmente ler e internalizar essas regras;)
Nicol Bolas
Na verdade, eu esqueci o constexemplo mínimo. Tenho certeza de que o Eigen usa em consttodos os lugares (ou fora das definições de classe, também com constreferências), mas preciso verificar. Eu tento quebrar o mecanismo geral que Eigen usa para um exemplo mínimo, quando encontro o tempo.
chtz 6/03
-1

Temos problemas semelhantes com nossos arquivos de cabeçalho Goopax. Compilar o seguinte com clang-10 e -std = c ++ 2a produz um erro do compilador.

template<typename T> class gpu_type;

using gpu_bool     = gpu_type<bool>;
using gpu_int      = gpu_type<int>;

template<typename T>
class gpu_type
{
  friend inline gpu_bool operator==(T a, const gpu_type& b);
  friend inline gpu_bool operator!=(T a, const gpu_type& b);
};

int main()
{
  gpu_int a;
  gpu_bool b = (a == 0);
}

Fornecer esses operadores adicionais parece resolver o problema:

template<typename T>
class gpu_type
{
  ...
  friend inline gpu_bool operator==(const gpu_type& b, T a);
  friend inline gpu_bool operator!=(const gpu_type& b, T a);
};
Ingo Josopait
fonte
11
Isso não seria algo útil antes? Caso contrário, como teria a == 0sido compilado ?
Nicol Bolas 8/03
Este não é realmente um problema semelhante. Como Nicol apontou, isso já não era compilado no C ++ 17. Ele continua a não compilar no C ++ 20, apenas por um motivo diferente.
Barry
Esqueci de mencionar: Nós também fornecemos operadores membros: gpu_bool gpu_type<T>::operator==(T a) const;e gpu_bool gpu_type<T>::operator!=(T a) const;com o C ++ - 17, isso funciona bem. Mas agora com clang-10 e C ++ - 20, eles não são mais encontrados e, em vez disso, o compilador tenta gerar seus próprios operadores trocando os argumentos e falha, porque o tipo de retorno não é bool.
Ingo Josopait