overload operator assignment c++ language-lawyer c++17 ternary-operator implicit-conversion

assignment - overload operator c++



Operador ternario conversión implícita a clase base (4)

Considera este pedazo de código:

struct Base { int x; }; struct Bar : Base { int y; }; struct Foo : Base { int z; }; Bar* bar = new Bar; Foo* foo = new Foo; Base* returnBase() { Base* obj = !bar ? foo : bar; return obj; } int main() { returnBase(); return 0; }

Esto no funciona bajo Clang o GCC, dándome:

error: la expresión condicional entre los distintos tipos de puntero ''Foo *'' y ''Bar *'' carece de una base de lanzamiento * obj =! bar? foo: bar;

Lo que significa que para que compile tengo que cambiar el código a:

Base* obj = !bar ? static_cast<Base*>(foo) : bar;

Dado que existe una conversión implícita a una Base* , ¿qué impide que el compilador lo haga?

En otras palabras, ¿por qué Base* obj = foo; ¿Trabaja sin un molde pero utilizando el operador?: no? ¿Es porque no está claro que quiera usar la parte Base ?


Dado que existe una conversión implícita a una Base* , ¿qué impide que el compilador lo haga?

Según [expr.cond] / 7,

En el segundo y tercer operandos se llevan a cabo conversiones estándar de valor a valor, de matriz a puntero y de función a puntero. Después de esas conversiones, se mantendrá una de las siguientes:

  • ...
  • Uno o ambos de los operandos segundo y tercero tienen tipo puntero; Las conversiones de puntero, las conversiones de puntero de función y las conversiones de calificación se realizan para llevarlos a su tipo de puntero compuesto. El resultado es del tipo de puntero compuesto.

donde el tipo de puntero compuesto se define en [expr.type] / 4:

El tipo de puntero compuesto de dos operandos p1 y p2 que tienen los tipos T1 y T2, respectivamente, donde al menos uno es un tipo de puntero o puntero a miembro o std :: nullptr_t, es:

  • si tanto p1 como p2 son constantes de puntero nulas, std :: nullptr_t;

  • si p1 o p2 es una constante de puntero nula, T2 o T1, respectivamente;

  • si T1 o T2 es "puntero a cv1 void" y el otro tipo es "puntero a cv2 T", donde T es un tipo de objeto o void, "puntero a cv12 void", donde cv12 es la unión de cv1 y cv2;

  • si T1 o T2 es "puntero a función de no excepción" y el otro tipo es "puntero a función", donde los tipos de función son los mismos, "puntero a función";

  • si T1 es "puntero a cv1 C1" y T2 es "puntero a cv2 C2", donde C1 está relacionado con la referencia a C2 o C2 está relacionado con la referencia a C1, el tipo combinado cv de T1 y T2 o el combinado cv tipo de T2 y T1, respectivamente;

  • si T1 es "puntero a miembro de C1 de tipo cv1 U1" y T2 es "puntero a miembro de C2 de tipo cv2 U2" donde C1 está relacionado con referencia a C2 o C2 está relacionado con referencia a C1, el tipo combinado de cv de T2 y T1 o el tipo combinado de CV de T1 y T2, respectivamente;

  • si T1 y T2 son tipos similares, el tipo combinado de CV de T1 y T2;

  • de lo contrario, un programa que necesita la determinación de un tipo de puntero compuesto está mal formado.

Ahora puede ver que un puntero a "base común" no es el tipo de puntero compuesto.

En otras palabras, ¿por qué Base* obj = foo; ¿Trabaja sin un molde pero utilizando el operador?: no? ¿Es porque no está claro que quiera usar la parte Base ?

El problema es que la regla debe detectar el tipo de expresión condicional de forma independiente, sin observar la inicialización.

Para ser específicos, una regla debe detectar las expresiones condicionales en las siguientes dos afirmaciones para que sean del mismo tipo.

Base* obj = !bar ? foo : bar; bar ? foo : bar;

Ahora, si no tiene dudas de que la expresión condicional en la segunda declaración está mal formada 1 , ¿cuál es el razonamiento para que esté bien formada en la primera declaración?

1 Por supuesto, uno puede hacer una regla para que dicha expresión esté bien formada. Por ejemplo, permita que los tipos de puntero compuesto incluyan el puntero a un tipo de base no ambiguo. Sin embargo, esto es algo más allá de esta pregunta, y debe ser discutido por el comité de ISO C ++.


En otras palabras, ¿por qué Base* obj = foo; ¿Trabaja sin un molde pero utilizando el operador?: no?

El tipo de expresión condicional no depende de a qué está asignado. En su caso, el compilador necesita poder evaluar !bar ? foo : bar; !bar ? foo : bar; independientemente de lo que se asigna a.

En su caso, eso es un problema, ya que ni foo convertido al tipo de bar ni la bar pueden convertirse al tipo de foo .

¿Es porque no está claro que quiera usar la parte Base?

Precisamente.


Citando el borrador estándar de C ++ N4296, Sección 5.16 Operador condicional , Párrafo 6.3:

  • Uno o ambos de los operandos segundo y tercero tienen tipo puntero; Las conversiones de puntero (4.10) y las conversiones de calificación (4.4) se realizan para llevarlas a su tipo de puntero compuesto (Cláusula 5). El resultado es del tipo de puntero compuesto.

Sección 5 Expresiones , párrafos 13.8 y 13.9:

El tipo de puntero compuesto de dos operandos p1 y p2 que tienen los tipos T1 y T2, respectivamente, donde al menos uno es un puntero o puntero al tipo de miembro o std :: nullptr_t, es:

  • si T1 y T2 son tipos similares (4.4), el tipo combinado Cv de T1 y T2;
  • de lo contrario, un programa que necesita la determinación de un tipo de puntero compuesto está mal formado .

Nota: Copié 5 / 13.8 aquí solo para mostrarte que no tiene éxito. Lo que realmente está en efecto es 5 / 13.9, "el programa está mal formado".

Y Sección 4.10 Conversiones de puntero , Párrafo 3:

Un prvalor de tipo "puntero a cv D", donde D es un tipo de clase, se puede convertir a un prvalor de tipo "puntero a cv B", donde B es una clase base (Cláusula 10) de D. Si B es un inaccesible (Cláusula 11) o ambigua (10.2) clase base de D, un programa que necesita esta conversión está mal formado. El resultado de la conversión es un puntero al subobjeto de clase base del objeto de clase derivado. El valor del puntero nulo se convierte al valor del puntero nulo del tipo de destino.

Por lo tanto, no importa (en absoluto) que tanto Foo como Bar se derivan de una misma clase base. Solo importa que un puntero a Foo y un puntero a Bar no sean convertibles entre sí (sin relación de herencia).


Permitir una conversión al tipo de puntero base para el operador condicional suena bien, pero sería problemático en la práctica.

En tu ejemplo

struct Base {}; struct Foo : Base {}; struct Bar : Base {};

Podría parecer la opción obvia para el tipo de cond ? foo : bar cond ? foo : bar para ser Base* .

Pero esa lógica no es válida para un caso general.

P.ej:

struct TheRealBase {}; struct Base : TheRealBase {}; struct Foo : Base {}; struct Bar : Base {};

Debe cond ? foo : bar cond ? foo : bar be de tipo Base* o de tipo TheRealBase* ?

Qué tal si:

struct Base1 {}; struct Base2 {}; struct Foo : Base1, Base2 {}; struct Bar : Base1, Base2 {};

¿Qué tipo de cond ? foo : bar debería cond ? foo : bar cond ? foo : bar ser ahora?

¿O qué tal ahora?

struct Base {}; struct B1 : Base {}; struct B2 : Base {}; struct X {}; struct Foo : B1, X {}; struct Bar : B2, X {}; Base / / / / / / B1 B2 | X | | / / | |/ / | Foo Bar

¡¡Ay!! ¿Buena suerte razonando para un tipo de cond ? foo : bar cond ? foo : bar . Lo sé, feo, feo, poco práctico y digno de ser cazado, pero el estándar aún tendría que tener reglas para esto.

Tú entiendes.

Y también tenga en cuenta que std::common_type se define en términos de las reglas del operador condicional.