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)