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 Array
objetos o Array
y 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?
Respuestas:
El problema de Eigen parece reducirse a lo siguiente:
Los dos candidatos para la expresión son
operator==(const Scalar&, const Derived&)
Base<X>::operator!=(const Scalar&) const
Por [over.match.funcs] / 4 , como
operator!=
no fue importado en el alcance deX
mediante una declaración de uso , el tipo del parámetro de objeto implícito para # 2 esconst 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:
using Base::operator!=;
aDerived
ooperator==
a tomar unconst Base&
lugar de unaconst Derived&
.fuente
bool
de suoperator==
? Porque esa parece ser la única razón por la cual el código está mal formado bajo las nuevas reglas.operator==(Array, Scalar)
elemento que hace una comparación entre elementos y devuelve unArray
debool
. No puedes convertir eso en unbool
sin romper todo lo demás.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 unbool
valor de retorno no es parte de la selección de candidatos para la reescritura.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):Esto proviene de las nuevas reglas de candidatos reescritas en [over.match.oper] /3.4 . Todos esos candidatos son viables, ya que nuestros
Foo
argumentos no lo sonconst
. 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 :
#2
y#3
son candidatos reescritos, y#3
ha invertido el orden de los parámetros, mientras#1
que 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.#1
es mejor que#2
porque todas las secuencias de conversión son iguales (trivialmente, porque los parámetros de la función son iguales) y#2
es un candidato reescrito mientras#1
no lo es.Pero ... ambos pares
#1
/#3
y#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
/#2
mientras que el segundo parámetro tiene una mejor secuencia de conversión#3
(el parámetro queconst
debe someterse a unaconst
calificación adicional , por lo que tiene una secuencia de conversión peor). Esteconst
flip-flop hace que no podamos preferir ninguno de los dos.Como resultado, toda la resolución de sobrecarga es ambigua.
Eso no es correcto Consideramos incondicionalmente candidatos reescritos y revertidos. La regla que tenemos es, desde [over.match.oper] / 9 :
Es decir, todavía consideramos a estos candidatos. Pero si el mejor candidato viable es
operator==
aquel que regresa, digamos,Meta
el 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
Meta
es irrelevante; el problema también existiría si regresarabool
.Afortunadamente, la solución aquí es fácil:
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.#1
ahora vencería#3
no reescribiendo y#2
ahora vencería#3
al no revertirse, lo que lo convierte en#1
el mejor candidato viable. El mismo resultado que tuvimos en C ++ 17, solo unos pocos pasos más para llegar allí.fuente
==
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í.cv bool
(y antes de este cambio, el requisito era la conversión contextual abool
- todavía no!
)[over.match.best] / 2 enumera cómo se priorizan las sobrecargas válidas en un conjunto. La Sección 2.8 nos dice que
F1
es mejor queF2
si (entre muchas otras cosas):El ejemplo allí muestra un
operator<
ser explícito llamado aunqueoperator<=>
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
const
funciones. Y hacer que noconst
causen 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 noconst
-a-const
necesitan 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.
fuente
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.const
en el ejemplo mínimo. Estoy bastante seguro de que Eigen usa enconst
todas partes (o fuera de las definiciones de clase, también conconst
referencias), pero necesito verificarlo. Intento desglosar el mecanismo general que usa Eigen en un ejemplo mínimo, cuando encuentro el momento.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.
Proporcionar estos operadores adicionales parece resolver el problema:
fuente
a == 0
contrario, ¿cómo habría compilado ?gpu_bool gpu_type<T>::operator==(T a) const;
ygpu_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 esbool
.