tipos datos conversion c++ virtual-inheritance downcasting static-cast

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



conversion c++ (6)

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

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] ), por lo que el código no se compila, porque virtualmente hereda de Base . Eliminar lo 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?


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

En su ejemplo, parece correcto, porque solo hay una clase a la vista con una Base base, por lo que parece irrelevante que la herencia sea virtual. Pero el compilador no sabe si alguien definió otra class Derived2 : public virtual Base, public Derived {} , y está emitiendo un Base* apuntando al subobjeto Base de eso. En general [*], el desplazamiento entre el subobjeto Base y el subobjeto Derived dentro de Derived2 podría no ser el mismo que el desplazamiento entre el subobjeto Base y el objeto Derived completo 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.

Su 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é por qué), pero supongo que sin verificar que es posible un dynamic_cast 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 ;-)


Fundamentalmente, no hay una razón real, pero la intención es que static_cast sea ​​muy barato, involucrando a lo sumo una suma o una resta de una constante al puntero. Y no hay forma de implementar el elenco que quieres tan barato; Básicamente, debido a que las posiciones relativas de Derived y Base dentro del objeto pueden cambiar si hay herencia adicional, la conversión requeriría una gran parte de la sobrecarga de dynamic_cast ; los miembros del comité probablemente pensaron que esto derrota las razones para usar static_cast lugar de dynamic_cast .


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.


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.

virtual ismo virtual 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 cv-cualificación 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 .


static_cast puede realizar solo aquellos static_cast 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 del lanzamiento, así como también leer la información del 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 la memoria entre la Base y Derived . ¿Está uno derecho tras otro o hay un espacio adicional? Debido a que 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 and Right , donde cada uno 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 utilizar 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 {...};

El uso de la herencia virtual garantiza que cuando se crea Bottom , contenga solo una copia de Base que se comparte entre las partes del objeto Left y Right . El diseño del objeto Bottom puede ser, por ejemplo:

Base part Left part Right part Bottom part

Ahora, considera que lanzas de Bottom a Right (que es un elenco válido). Obtiene un puntero Right a un objeto que está dividido en dos partes: Base y Right tienen un espacio de memoria entre ellas, que contiene la parte Left (ahora irrelevante). La información sobre este espacio se almacena en tiempo de ejecución en un campo oculto de Right (generalmente denominado vbase_offset ). Puede leer los detalles, por ejemplo, here .

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

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

Nota sobre dynamic_cast:

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