Conversión implícita del operador ternario a la clase base

11 minutos de lectura

avatar de usuario
Gallo con sombrero

Considere esta pieza 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 conversión Base* obj = !bar ? foo : bar;

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

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

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

En otras palabras, ¿por qué Base* obj = foo; trabajar sin yeso pero utilizando el ?: el operador no? ¿Es porque no está claro que quiero usar el Base ¿parte?

  • Es hora de que entren los abogados. Esto tiene mi voto como la pregunta del día.

    – Betsabé

    12 de marzo de 2018 a las 16:26

  • Por favor: en.cppreference.com/w/cpp/language/…

    – 463035818_no_es_un_número

    12 de marzo de 2018 a las 16:26

  • “Dado que existe una conversión implícita a una Base *, ¿qué impide que el compilador lo haga?” – ¿Por qué debería intentarlo? El compilador intenta y convierte de Foo * a Bar * y falla; intenta una conversión de Bar * a Foo * y fallar ¿Por qué debería intentar una conversión de ambos a Base *? Y si hay bases más comunes, ¿qué puntero elegir?

    – max66

    12/03/2018 a las 16:35

  • @DrewDormann no lo leí con atención. Por lo que vi, esperaría que se permitieran las conversiones derivadas a base

    – 463035818_no_es_un_número

    12 de marzo de 2018 a las 16:36

  • Conversión implícita a void* existe para ambos tipos, pero no esperamos que esta conversión realmente se active, ¿verdad?

    – norte 1.8e9-dónde-está-mi-participación m.

    12 de marzo de 2018 a las 16:51

avatar de usuario
iBug

Cita del borrador estándar N4296 de C++, Sección 5.16 operador condicionalPárrafo 6.3:

  • Uno o ambos operandos segundo y tercero tienen tipo de puntero; conversiones de puntero (4.10) y conversiones de calificaciones (4.4) se realizan para llevarlos a su tipo de puntero compuesto (Cláusula 5). El resultado es del tipo puntero compuesto.

Sección 5 ExpresionesPárrafo 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 un 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 requiere la determinación de un tipo de puntero compuesto está mal formado.

Nota: copié 5/13.8 aquí solo para mostrarles que no funciona. Lo que realmente está en vigor es 5/13.9, “el programa está mal formado”.

Y la Sección 4.10 Conversiones de punteroPárrafo 3:

Un prvalue de tipo “apuntador a cv D”, donde D es un tipo de clase, se puede convertir a un prvalue de tipo “apuntador a cv B”, donde B es una clase base (Cláusula 10) de D. Si B es un clase base de D inaccesible (cláusula 11) o ambigua (10.2), 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 en el valor del puntero nulo del tipo de destino.

Por lo tanto, no importa (en absoluto) que tanto Foo como Bar se deriven 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).

  • El problema con el código es que no puede determinar el tipo de puntero compuesto porque cae en la viñeta final que dice que un programa que requiere tal determinación está mal formado, ya que no se cumple ninguna de las otras viñetas. Podría decirse que la viñeta más cercana es la “relacionada con la referencia”, ya que esa es la viñeta que maneja los punteros derivados/base. “Similar” básicamente significa “mismo tipo ignorando la calificación de cv en todos los niveles”, lo que claramente no se cumple porque estos son indicadores de diferentes tipos. Las reglas de conversión de punteros son irrelevantes; ni siquiera puede determinar el tipo al que convertirlos.

    – CT

    13 de marzo de 2018 a las 0:18

avatar de usuario
bolov

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

Puede parecer la elección obvia para el tipo de cond ? foo : bar ser – estar Base*.

Pero esa lógica no se sostiene para un caso general

P.ej:

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

Debería cond ? foo : bar ser de tipo Base* o de tipo TheRealBase*?

Qué tal si:

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

que tipo debe 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. 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 ten en cuenta que std::common_type se define en términos de las reglas del operador condicional.


Hm… Estos son excelentes ejemplos de situaciones que no pudieron funcionar debido a la ambigüedad. Pero C++ tiene muchas reglas de conversión que funcionan siempre que la conversión no sea ambigua. Como en esta pregunta. ¿No?

Permitir aquí solo el caso inequívoco sería extremadamente problemático. El simple hecho de agregar una clase base podría hacer que el programa no se pueda compilar:

Base de código inicial:

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

Esto permitiría __ ? foo : bar; para ser escrito. Y ahora no puede modificar la estructura de herencia porque casi cualquier modificación rompería el código existente que usa el operador ternario de manera legítima:

struct FooBarCommon {};

struct Base {};
struct Foo : Base, FooBarCommon {};
struct Bar : Base, FooBarCommon {};
struct Baz : Base {};

Esto parece una modificación razonable. Tal como están las reglas ahora, puede hacer esto siempre que no modifique la API pública de sus clases. Esto ya no será cierto, el estándar permitiría las conversiones a la clase base solo en casos inequívocos.

  • Hm… Estos son excelentes ejemplos de situaciones que no pude trabajar, debido a la ambigüedad. Pero C++ tiene muchas reglas de conversión que hacer trabajo siempre que la conversión no sea ambigua. Como en esta pregunta. ¿No?

    – Drew Dorman

    12 de marzo de 2018 a las 17:13

  • No veo cómo hay ningún problema en la práctica. Ha descrito casos específicos en los que el compilador no pudo determinar fácilmente cuál sería el tipo de retorno del operador ternario, pero estos no son todos los casos. Ya tenemos reglas para determinar el tipo de devolución de un operador ternario que excluyen el ejemplo de la pregunta y los ejemplos que proporciona. ¿Por qué no podríamos expandir las reglas para incluir el caso en el ejemplo de la pregunta y excluir los casos en sus ejemplos? Es fácil saber cuándo hay ambigüedad, entonces, ¿por qué no limitar las reglas a los casos que no son ambiguos?

    – Jordán Melo

    14/03/2018 a las 19:32

avatar de usuario
R Sahú

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

El tipo de la expresión condicional no depende de a qué se le asigne. En su caso, el compilador debe poder evaluar !bar ? foo : bar; independientemente de a qué se le asigne.

En tu caso, eso es un problema ya que ni foo convertido a tipo de bar ni bar se puede convertir en tipo de foo.

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

Precisamente.

  • “El tipo de una expresión no depende de a qué se le asigne”. Eso no es cierto en general. Una expresión que nombra un conjunto sobrecargado de funciones no puede tipificarse per se (p. ej., decltype falla), pero obtiene un tipo adecuado en el contexto correspondiente. Tal vez ajuste eso a “tipo de condicional …”.

    – Colombo

    12 de marzo de 2018 a las 16:42


  • @Columbo, ¿puede señalarme algún lugar donde pueda obtener una comprensión más profunda del tema?

    – R Sahu

    12 de marzo de 2018 a las 16:46

  • Esto es algo básico; sobrecargar un nombre f y escribe F* ptr = f;. No veo qué hay que aclarar más.

    – Colombo

    12 de marzo de 2018 a las 16:54


  • Disculpe si surgió de manera abrasiva, simplemente no sabía a qué señalar: ¿la cláusula 5 en el estándar? :s

    – Colombo

    12 de marzo de 2018 a las 17:21

  • @Columbo, está bien. A pesar de años de usar C++, todavía hay algunos aspectos del lenguaje que no conozco y algunos aspectos del lenguaje que no me llaman la atención.

    – R Sahu

    12 de marzo de 2018 a las 17:29

avatar de usuario
xskxzr

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

De acuerdo a [expr.cond]/7,

Las conversiones estándar de valor L a valor R, de matriz a puntero y de función a puntero se realizan en el segundo y tercer operandos. Después de esas conversiones, se cumplirá uno de los siguientes:

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

dónde 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 puntero o un tipo de puntero a miembro o std:: nullptr_t, es:

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

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

  • si T1 o T2 es “pointer to cv1 void” y el otro tipo es “pointer to cv2 T”, donde T es un tipo de objeto o void, “pointer to cv12 void”, donde cv12 es la unión de cv1 y cv2;

  • si T1 o T2 es “puntero a ninguna función excepto” 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 referencia a C2 o C2 está relacionado con referencia a C1, el tipo combinado cv de T1 y T2 o el tipo combinado cv tipo de T2 y T1, respectivamente;

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

  • si T1 y T2 son tipos similares, 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.

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; trabajar sin yeso pero utilizando el ?: el operador no? ¿Es porque no está claro que quiero usar el Base ¿parte?

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

Para ser específicos, una regla debe detectar que las expresiones condicionales en las siguientes dos declaraciones son 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 formada1¿cuál es el razonamiento para hacerlo bien formado en la primera declaración?


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

exp1 ? exp2 : exp3

En el operador condicional, exp2 y exp3 deben ser del mismo tipo o, al menos, deben tener una función de conversión de tipo que convierta un tipo en otro. Si no, entonces está mal formado.

    int i = 1;
    long j = 2;
    true  ? i : j; // OK
    false ? i : j; // OK


    string str1 = "hello";
    const char* str2 = "world";
    true  ? str1 : str2; // OK
    false ? str1 : str2; // OK

    int i = 1;
    string str1 = "hello";
    true  ? i : str1; // Error: No conversion from 'std::string' to 'int'
    false ? i : str1; // Error: No conversion from 'std::string' to 'int'

¿Ha sido útil esta solución?