Pregunta ¿Por qué no se puede utilizar static_cast para down-cast cuando se trata de herencia virtual?


Considera el siguiente código:

struct Base {};
struct Derived : public virtual Base {};

void f()
{
    Base* b = new Derived;
    Derived* d = static_cast<Derived*>(b);
}

Esto está prohibido por el estándar ([n3290: 5.2.9/2]) entonces el código no compila, porque Derived  virtualmente hereda de Base. La eliminación de la virtual de la herencia hace que el código sea válido.

¿Cuál es la razón técnica para que exista esta regla?


32
2017-09-20 12:10


origen


Respuestas:


El problema técnico es que no hay forma de resolver un problema Base* cuál es el desplazamiento entre el inicio del Base subobjeto y el inicio de la Derived objeto.

En su ejemplo, parece correcto, porque solo hay una clase a la vista con un Base base, por lo que parece irrelevante que la herencia sea virtual. Pero el compilador no sabe si alguien definió otro class Derived2 : public virtual Base, public Derived {}, y está lanzando un Base* señalando el Base subobjeto de eso. En general [*], la compensación entre Base subobjeto y el Derived subobjeto dentro Derived2 podría no ser el mismo que el desplazamiento entre el Base subobjeto y el completo Derived objeto de un objeto cuyo tipo más derivado es Derived, precisamente porque Base es virtualmente heredado

De modo que no hay forma de conocer el tipo dinámico del objeto completo, y las diferentes compensaciones entre el puntero que le ha dado al elenco y el resultado requerido, dependiendo de qué tipo de ese tipo sea dinámico. Por lo tanto, el elenco es imposible.

Tu Base no tiene funciones virtuales y, por lo tanto, no tiene RTTI, por lo que ciertamente no hay forma de decir el tipo del objeto completo. El elenco aún está prohibido, incluso si Base tiene RTTI (no sé de inmediato por qué), pero supongo que sin verificar que dynamic_cast es posible en ese caso.

[*] con lo que quiero decir, si este ejemplo no prueba el punto, sigue agregando más herencia virtual hasta que encuentres un caso en el que las compensaciones sean diferentes ;-)


33
2017-09-20 12:30



Considere la siguiente función foo:

#include <iostream>

struct A
{
    int Ax;
};

struct B : virtual A
{
    int Bx;
};

struct C : B, virtual A
{
    int Cx;
};


void foo( const B& b )
{
    const B* pb = &b;
    const A* pa = &b;

    std::cout << (void*)pb << ", " << (void*)pa << "\n";

    const char* ca = reinterpret_cast<const char*>(pa);
    const char* cb = reinterpret_cast<const char*>(pb);

    std::cout << "diff " << (cb-ca) << "\n";
}

int main(int argc, const char *argv[])
{
    C c;
    foo(c);

    B b;
    foo(b);
}

Aunque no es realmente portátil, esta función nos muestra el "desplazamiento" de A y B. Dado que el compilador puede ser muy liberal al colocar el subobjeto A en caso de herencia (¡también recuerde que el objeto más derivado llama a la base virtual ctor!), la ubicación real depende del tipo "real" del objeto. Pero como foo solo obtiene una referencia a B, cualquier static_cast (que trabaje en el tiempo de compilación como máximo aplicando algún offset) está destinado a fallar.

Resultados de ideone.com (http://ideone.com/2qzQu) para esto:

0xbfa64ab4, 0xbfa64ac0
diff -12
0xbfa64ac4, 0xbfa64acc
diff -8

2
2017-09-20 12:48



Fundamentalmente, no hay una razón real, pero la intención es que static_cast ser muy barato, involucrar como máximo una adición o una resta de una constante al puntero. Y no hay forma de implementar el elenco que desea de manera económica; básicamente, porque el posiciones relativas de Derived y Base dentro del objeto puede cambiar si hay herencia adicional, la conversión requeriría un buen trato de la sobrecarga de dynamic_cast; los miembros del comité Probablemente pensó que esto derrota las razones para usar static_casten lugar de dynamic_cast.


2
2017-09-20 12:55



static_cast es una construcción en tiempo de compilación. comprueba la validez de la conversión en tiempo de compilación y proporciona un error de compilación si la conversión no es válida.

virtualismo es un fenómeno de tiempo de ejecución.

Ambos no pueden ir juntos.

C ++ 03 Norma §5.2.9 / 2 y §5.2.9 / 9 son relevantes en este caso.

Un valor de tipo "puntero a cv1 B", donde B es un tipo de clase, se puede convertir a un valor r de tipo "puntero a cv2 D", donde D es una clase derivada (cláusula 10) de B, si es un estándar válido la conversión de "puntero a D" a "puntero a B" existe (4.10), cv2 es la misma calificación cv, o mayor calificación cv que, cv1, y B no es una clase base virtual de D. El valor del puntero nulo (4.10) se convierte al valor del puntero nulo del tipo de destino. Si el valor r de tipo "puntero a cv1 B" apunta a un B que en realidad es un subobjeto de un objeto de tipo D, el puntero resultante apunta al objeto circundante de tipo D. De lo contrario, el resultado del molde no está definido .


1
2017-09-20 12:15



Supongo que esto se debe a que las clases con herencia virtual tienen diferentes diseños de memoria. El padre debe ser compartido entre los niños, por lo tanto, solo uno de ellos se puede distribuir continuamente. Eso significa que no se garantiza que pueda separar un área continua de memoria para tratarlo como un objeto derivado.


1
2017-09-20 12:26



static_cast puede realizar solo aquellos lanzamientos donde el diseño de la memoria entre las clases se conoce en tiempo de compilación. dynamic_cast puede verificar la información en tiempo de ejecución, lo que permite verificar con mayor precisión la corrección de conversión, así como también leer la información de tiempo de ejecución con respecto al diseño de la memoria.

La herencia virtual pone una información en tiempo de ejecución en cada objeto que especifica cuál es el diseño de memoria entre el Base y Derived. ¿Está uno derecho tras otro o hay un espacio adicional? Porque static_cast no puede acceder a dicha información, el compilador actuará de manera conservadora y solo dará un error de compilación.


Con más detalle:

Considere una estructura de herencia compleja, donde, debido a la herencia múltiple, hay varias copias de Base. El escenario más típico es una herencia de diamantes:

class Base {...};
class Left : public Base {...};
class Right : public Base {...};
class Bottom : public Left, public Right {...};

En este escenario Bottom consiste en Left y Right, dónde cada tiene su propia copia de Base. La estructura de memoria de todas las clases anteriores se conoce en tiempo de compilación y static_cast se puede usar sin problemas

Consideremos ahora la estructura similar pero con la herencia virtual de Base:

class Base {...};
class Left : public virtual Base {...};
class Right : public virtual Base {...};
class Bottom : public Left, public Right {...};

Usar la herencia virtual asegura que cuando Bottom se crea, contiene solo uno copia de Base es decir compartido entre partes del objeto Left y Right. El diseño de Bottom objeto puede ser, por ejemplo:

Base part
Left part
Right part
Bottom part

Ahora, considera que lanzas Bottom a Right (que es un elenco válido). Obtienes un Right puntero a un objeto que está en dos partes: Base y Right tener una brecha de memoria en el medio, que contiene el (ahora irrelevante) Left parte. La información sobre este espacio se almacena en tiempo de ejecución en un campo oculto de Right(por lo general se conoce como vbase_offset) Puede leer los detalles, por ejemplo aquí.

Sin embargo, la brecha no existiría si solo crease una independiente Right objeto.

Entonces, si te doy solo un puntero a Right no sabe en tiempo de compilación si se trata de un objeto independiente o parte de algo más grande (p. Bottom) Necesita verificar la información de tiempo de ejecución para emitir correctamente desde Right a Base. Es por eso que static_cast fallará y dynamic_cast no lo hará


Nota sobre dynamic_cast:

Mientras static_cast no utiliza información en tiempo de ejecución sobre el objeto, dynamic_cast usos y requiere ¡para existir! Por lo tanto, este último elenco se puede usar solo en aquellas clases que contienen al menos una función virtual (por ejemplo, un destructor virtual)


0
2018-06-09 21:30