c++ - registro - ejemplo de instancia o solicitud
¿Está permitido escribir una instancia de Derivado sobre una instancia de Base? (4)
Diga, el código
class Derived: public Base {....}
Base* b_ptr = new( malloc(sizeof(Derived)) ) Base(1);
b_ptr->f(2);
Derived* d_ptr = new(b_ptr) Derived(3);
b_ptr->g(4);
d_ptr->f(5);
parece ser razonable y LSP está satisfecho.
Sospecho que este código está permitido de forma estándar cuando Base y Derived son POD, y no se permiten de otra manera (porque vtbl ptr se sobrescribe). La primera parte de mi pregunta es: por favor, señale la precondición precisa de dicha sobrescritura.
Puede haber otras formas estándar permitidas para escribir.
La segunda parte de mi pregunta es: ¿hay otras formas? ¿Cuáles son sus precondiciones precisas?
ACTUALIZACIÓN: NO quiero escribir código como este; Estoy interesado en la posibilidad teórica (o imposibilidad) de tal código. Entonces, esta es una pregunta "estándar nazi", no una pregunta "¿cómo puedo ...". (¿Mi pregunta se ha movido a otro sitio stackoverflow?)
ACTUALIZACIÓN2 y 4: ¿Qué hay de los destructores? La semántica que se supone de este código es "La instancia base se actualiza (destructivamente) por porción de instancia Derivada". Supongamos, en aras de la simplicidad, que la clase Base tiene un destructor trivial.
ACTUALIZACIÓN3: Lo más interesante para mí es la validez del acceso a través de b_ptr->g(4)
Creo que la sobreescritura está permitida.
Si fuera yo, probablemente llamaría Base::~Base
antes de reutilizar el almacenamiento, para que la vida útil del objeto original finalice limpiamente. Pero el estándar explícitamente le permite reutilizar el almacenamiento sin llamar al destructor.
No creo que tu acceso a través de b_ptr sea válido. La vida útil del objeto Base ha terminado.
(Ver 3.8 / 4 en cualquiera de los estándares para conocer las reglas de por vida).
Y tampoco estoy del todo convencido de que b_ptr deba dar la misma dirección que la llamada malloc ().
Estás inicializando la misma pieza de memoria dos veces. Eso no terminará bien.
Supongamos, por ejemplo, que el constructor Base
asigna algo de memoria y la almacena en un puntero. La segunda vez a través del constructor, se sobrescribirá el primer puntero y se filtrará la memoria.
Realmente necesita hacer b_ptr = d_ptr
después de la colocación-new de Derived
, en caso de que el subobjeto Base
no sea el primero en el diseño de Derived
. Como está escrito, b_ptr->g(4)
evoca un comportamiento indefinido.
La regla (3.8 basic.life
):
Si, una vez finalizado el tiempo de vida de un objeto y antes de que se reutilice o libere el almacenamiento que ocupa el objeto, se crea un nuevo objeto en la ubicación de almacenamiento ocupada por el objeto original, un puntero que apunta al objeto original , una referencia que referido al objeto original, o el nombre del objeto original se referirá automáticamente al nuevo objeto y, una vez que se haya iniciado el tiempo de vida del nuevo objeto, se puede usar para manipular el nuevo objeto, si :
- el almacenamiento para el nuevo objeto se superpone exactamente a la ubicación de almacenamiento que ocupó el objeto original, y
- el nuevo objeto es del mismo tipo que el objeto original (ignorando los cv-quali fires de nivel superior), y
- el tipo del objeto original no está const-quali fi cado y, si es un tipo de clase, no contiene ningún miembro de datos no estático cuyo tipo esté const-quali fi cado o un tipo de referencia, y
- el objeto original era el objeto más derivado (1.8) de tipo
T
y el nuevo objeto es el objeto más derivado de tipoT
(es decir, no son subobjetos de clase base ).
Probablemente también deberías estar destruyendo el objeto viejo antes de reutilizar su memoria, pero el estándar no lo exige. Sin embargo, si no lo hace, se perderá cualquier recurso propiedad del objeto anterior. La regla completa se da en la sección 3.8 ( basic.life
) del Estándar:
Un programa puede finalizar la vida útil de cualquier objeto mediante la reutilización del almacenamiento que ocupa el objeto o llamando explícitamente al destructor para un objeto de un tipo de clase con un destructor no trivial. Para un objeto de un tipo de clase con un destructor no trivial, no es necesario que el programa llame al destructor explícitamente antes de que se reutilice o libere el almacenamiento que ocupa el objeto; sin embargo, si no hay una llamada explícita al destructor o si una expresión de eliminación (5.3.5) no se utiliza para liberar el almacenamiento, el destructor no se llamará implícitamente y cualquier programa que dependa de los efectos secundarios producidos por el destructor tiene un comportamiento indefinido
Si escribe este código más limpiamente, es más fácil ver qué falla:
void * addr = std::malloc(LARGE_NUMBER);
Base * b = new (addr) Base;
b->foo(); // no problem
Derived * d = new (addr) Derived;
d->bar(); // also fine (#1)
b->foo(); // Error! b no longer points to a Base!
static_cast<Base*>(d)->foo(); // OK
b = d; b->foo(); // also OK
El problema es que en la línea marcada (n. ° 1), b
y d
apuntan a cosas totalmente independientes, no relacionadas, y ya que sobrescribió la memoria del antiguo objeto *b
, b
de hecho ya no es válida.
Es posible que tenga algunos pensamientos equivocados acerca de Base*
y Derived*
como tipos de punteros convertibles, pero eso no tiene nada que ver con la situación actual, y por el bien de este ejemplo, los dos tipos pueden no estar del todo relacionados. Es solo una de las dos últimas líneas que utilizamos el hecho de que Derived*
es convertible a Base*
, cuando realizamos la conversión real. Pero tenga en cuenta que esta conversión es una conversión de valor real, y d
no es el mismo puntero que static_cast<Base*>(d)
(al menos en lo que respecta al idioma).
Finalmente, limpiemos este desastre:
d->~Derived();
std::free(addr);
La oportunidad de destruir el *b
original ha pasado, por lo que podemos haber filtrado eso.