¿Cambio de ruptura en C ++ 20 o regresión en clang-trunk / gcc-trunk cuando se sobrecarga la comparación de igualdad con el valor de retorno no booleano?

11

El siguiente código se compila bien con clang-trunk en el modo c ++ 17 pero se rompe en el 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{});
}

También se compila bien con gcc-trunk o clang-9.0.0: https://godbolt.org/z/8GGT78

El error con clang-trunk y -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)

Entiendo que C ++ 20 hará posible solo sobrecargar operator==y el compilador generará automáticamente operator!=al negar el resultado de operator==. Por lo que yo entiendo, esto solo funciona mientras el tipo de retorno sea bool.

La fuente del problema es que en Eigen declaramos un conjunto de operadores ==, !=, <, ... entre Arrayobjetos o Arrayy escalares, que devuelven (una expresión de) una matriz de bool(que luego se puede acceder elemento a elemento, o utilizado de otra manera ) P.ej,

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

En contraste con mi ejemplo anterior, esto incluso falla con gcc-trunk: https://godbolt.org/z/RWktKs . Todavía no he logrado reducir esto a un ejemplo que no sea Eigen, que falla tanto en clang-trunk como en gcc-trunk (el ejemplo en la parte superior está bastante simplificado).

Informe de problema relacionado: https://gitlab.com/libeigen/eigen/issues/1833

Mi pregunta real: ¿es esto realmente un cambio radical en C ++ 20 (y existe la posibilidad de sobrecargar los operadores de comparación para devolver Metaobjetos), o es más probable una regresión en clang / gcc?

chtz
fuente

Respuestas:

5

El problema de Eigen parece reducirse a lo siguiente:

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

Los dos candidatos para la expresión son

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

Por [over.match.funcs] / 4 , como operator!=no fue importado en el alcance de Xmediante una declaración de uso , el tipo del parámetro de objeto implícito para # 2 es const Base<X>&. Como resultado, el # 1 tiene una mejor secuencia de conversión implícita para ese argumento (coincidencia exacta, en lugar de la conversión derivada a base). Al seleccionar # 1, el programa se vuelve mal formado.

Posibles soluciones:

  • Agregar using Base::operator!=;a Derivedo
  • Cambiar el operator==a tomar un const Base&lugar de una const Derived&.
TC
fuente
¿Hay alguna razón por la cual el código real no puede devolver un boolde su operator==? Porque esa parece ser la única razón por la cual el código está mal formado bajo las nuevas reglas.
Nicol Bolas
44
El código real implica un operator==(Array, Scalar)elemento que hace una comparación entre elementos y devuelve un Arrayde bool. No puedes convertir eso en un boolsin romper todo lo demás.
TC
2
Esto parece un defecto en el estándar. Se operator==suponía que las reglas para la reescritura no afectarían el código existente, pero lo hacen en este caso, porque la verificación de un boolvalor de retorno no es parte de la selección de candidatos para la reescritura.
Nicol Bolas
2
@NicolBolas: El principio general que se sigue es que la verificación es si puede hacer algo ( por ejemplo , invocar al operador), no si debe hacerlo , para evitar que los cambios de implementación afecten silenciosamente la interpretación de otro código. Resulta que las comparaciones reescritas rompen muchas cosas, pero sobre todo cosas que ya eran cuestionables y fáciles de solucionar. Entonces, para bien o para mal, estas reglas fueron adoptadas de todos modos.
Davis Herring
Wow, muchas gracias, supongo que su solución resolverá nuestro problema (no tengo tiempo para instalar gcc / clang trunk con un esfuerzo razonable en este momento, por lo que solo comprobaré si esto rompe algo con las últimas versiones estables del compilador )
chtz
11

Sí, el código de hecho se rompe en C ++ 20.

La expresión Foo{} != Foo{}tiene tres candidatos en C ++ 20 (mientras que solo había uno en 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

Esto proviene de las nuevas reglas de candidatos reescritas en [over.match.oper] /3.4 . Todos esos candidatos son viables, ya que nuestros Fooargumentos no lo son const. Para encontrar el mejor candidato viable, tenemos que pasar por nuestros desempates.

Las reglas relevantes para la mejor función viable son, desde [over.match.best] / 2 :

Dadas estas definiciones, una función viable F1se define como una mejor función que otra función viable F2si, para todos los argumentos i, no es una secuencia de conversión peor que , y luego ICSi(F1)ICSi(F2)

  • [... muchos casos irrelevantes para este ejemplo ...] o, si no eso, entonces
  • F2 es un candidato reescrito ([over.match.oper]) y F1 no es
  • F1 y F2 son candidatos reescritos, y F2 es un candidato sintetizado con un orden inverso de parámetros y F1 no es

#2y #3son candidatos reescritos, y #3ha invertido el orden de los parámetros, mientras #1que no se reescribe. Pero para llegar a ese desempate, primero debemos superar esa condición inicial: para todos los argumentos, las secuencias de conversión no son peores.

#1es mejor que #2porque todas las secuencias de conversión son iguales (trivialmente, porque los parámetros de la función son iguales) y #2es un candidato reescrito mientras #1no lo es.

Pero ... ambos pares #1/ #3y #2/ #3 quedan atrapados en esa primera condición. En ambos casos, el primer parámetro tiene una mejor secuencia de conversión para #1/ #2mientras que el segundo parámetro tiene una mejor secuencia de conversión #3(el parámetro que constdebe someterse a una constcalificación adicional , por lo que tiene una secuencia de conversión peor). Este constflip-flop hace que no podamos preferir ninguno de los dos.

Como resultado, toda la resolución de sobrecarga es ambigua.

Por lo que yo entiendo, esto solo funciona mientras el tipo de retorno sea bool.

Eso no es correcto Consideramos incondicionalmente candidatos reescritos y revertidos. La regla que tenemos es, desde [over.match.oper] / 9 :

Si operator==se selecciona un candidato reescrito por resolución de sobrecarga para un operador @, su tipo de retorno será cv bool

Es decir, todavía consideramos a estos candidatos. Pero si el mejor candidato viable es operator==aquel que regresa, digamos, Metael resultado es básicamente el mismo que si ese candidato fuera eliminado.

Nos hicieron no queremos estar en un estado donde la resolución de sobrecarga tendría que considerar el tipo de retorno. Y, en cualquier caso, el hecho de que el código aquí regrese Metaes irrelevante; el problema también existiría si regresara bool.


Afortunadamente, la solución aquí es fácil:

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

Una vez que realiza ambos operadores de comparación const, no hay más ambigüedad. Todos los parámetros son iguales, por lo que todas las secuencias de conversión son trivialmente iguales. #1ahora vencería #3no reescribiendo y #2ahora vencería #3al no revertirse, lo que lo convierte en #1el mejor candidato viable. El mismo resultado que tuvimos en C ++ 17, solo unos pocos pasos más para llegar allí.

Barry
fuente
" No queríamos estar en un estado donde la resolución de sobrecarga tuviera que considerar el tipo de retorno " . Solo para ser claros, mientras que la resolución de sobrecarga en sí misma no considera el tipo de retorno, las operaciones reescritas posteriores sí lo hacen . El código de uno está mal formado si la resolución de sobrecarga seleccionaría una reescritura ==y el tipo de retorno de la función seleccionada no lo está bool. Pero este sacrificio no ocurre durante la resolución de sobrecarga en sí.
Nicol Bolas
¡De hecho, solo está mal formado si el tipo de retorno es algo que no admite el operador! ...
Chris Dodd
1
@ChrisDodd No, tiene que ser exactamente cv bool(y antes de este cambio, el requisito era la conversión contextual a bool- todavía no !)
Barry
Desafortunadamente, esto no resuelve mi problema real, pero fue porque no pude proporcionar una ERM que realmente describe mi problema. Aceptaré esto y cuando pueda reducir mi problema adecuadamente, haré una nueva pregunta ...
chtz
2
Parece que una reducción adecuada para el problema original es gcc.godbolt.org/z/tFy4qz
TC
5

[over.match.best] / 2 enumera cómo se priorizan las sobrecargas válidas en un conjunto. La Sección 2.8 nos dice que F1es mejor que F2si (entre muchas otras cosas):

F2es un candidato reescrito ([over.match.oper]) y F1no es

El ejemplo allí muestra un operator<ser explícito llamado aunque operator<=>esté allí.

Y [over.match.oper] /3.4.3 nos dice que la candidatura de operator==en esta circunstancia es un candidato reescrito.

Sin embargo , sus operadores olvidan una cosa crucial: deberían ser constfunciones. Y hacer que no constcausen que los aspectos anteriores de la resolución de sobrecarga entren en juego. Ninguna de las funciones es una coincidencia exacta, ya que las conversiones no const-a- constnecesitan suceder para diferentes argumentos. Eso causa la ambigüedad en cuestión.

Una vez que los hagas const, compila el tronco Clang .

No puedo hablar con el resto de Eigen, ya que no conozco el código, es muy grande y, por lo tanto, no cabe en un MCVE.

Nicol Bolas
fuente
2
Solo llegamos al desempate que usted enumeró si hay conversiones igualmente buenas para todos los argumentos. Pero no hay: debido a la falta const, los candidatos no invertidos tienen una mejor secuencia de conversión para el segundo argumento y el candidato invertido tiene una mejor secuencia de conversión para el primer argumento.
Richard Smith
@ RichardSmith: Sí, ese era el tipo de complejidad del que estaba hablando. Pero no quería tener que pasar y leer / internalizar esas reglas;)
Nicol Bolas
De hecho, olvidé el consten el ejemplo mínimo. Estoy bastante seguro de que Eigen usa en consttodas partes (o fuera de las definiciones de clase, también con constreferencias), pero necesito verificarlo. Intento desglosar el mecanismo general que usa Eigen en un ejemplo mínimo, cuando encuentro el momento.
chtz
-1

Tenemos problemas similares con nuestros archivos de encabezado Goopax. Compilar lo siguiente con clang-10 y -std = c ++ 2a produce un error de compilación.

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

Proporcionar estos operadores adicionales parece resolver el 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
fuente
1
¿No era esto algo que sería útil haber hecho de antemano? De loa == 0 contrario, ¿cómo habría compilado ?
Nicol Bolas
Esto no es realmente un problema similar. Como señaló Nicol, esto ya no se compiló en C ++ 17. Sigue sin compilarse en C ++ 20, solo por una razón diferente.
Barry
Olvidé mencionar: También proporcionamos operadores miembros: gpu_bool gpu_type<T>::operator==(T a) const;y gpu_bool gpu_type<T>::operator!=(T a) const;con C ++ - 17, esto funciona bien. Pero ahora con clang-10 y C ++ - 20, ya no se encuentran, y en su lugar el compilador intenta generar sus propios operadores intercambiando los argumentos, y falla, porque el tipo de retorno no lo es bool.
Ingo Josopait