poo - ¿Cómo encuentra el operador de eliminación de C++ la ubicación de la memoria de un objeto polimórfico?
herencia c++ (7)
Me gustaría saber cómo el operador de eliminación detecta la ubicación de la memoria que debe liberarse cuando se le asigna un puntero de clase base que es diferente de la ubicación de la memoria verdadera del objeto.
Quiero duplicar este comportamiento en mi propio asignador / desasignador personalizado.
Considere la siguiente jerarquía:
struct A
{
unsigned a;
virtual ~A() { }
};
struct B
{
unsigned b;
virtual ~B() { }
};
struct C : public A, public B
{
unsigned c;
};
Quiero asignar un objeto de tipo C y eliminarlo a través de un puntero de tipo B. Por lo que puedo decir, este es un uso válido de delete de operador, y funciona bajo Linux / GCC:
C* c = new C;
B* b = c;
delete b;
Lo interesante es que los punteros "b" y "c" apuntan a diferentes direcciones debido a la forma en que el objeto se presenta en la memoria, y el operador de eliminación "sabe" cómo encontrar y liberar la ubicación correcta de la memoria.
Sé que, en general, no es posible encontrar el tamaño de un objeto polimórfico dado un puntero de clase base: averiguar el tamaño de un objeto polimórfico . Sospecho que generalmente no es posible encontrar la ubicación verdadera de la memoria del objeto.
Notas:
- Mi pregunta no está relacionada con el funcionamiento nuevo [] y eliminar []. Estoy interesado en el caso de asignación de objeto único. ¿Cómo se elimina [] "saber" el tamaño de la matriz de operandos? .
- Tampoco me preocupa cómo se llama al destructor. Estoy interesado en la desasignación de la memoria misma. Cómo funciona ''eliminar'' cuando elimino un puntero de la clase base
- Probé utilizando -fno-rtti y -fno-excepciones, por lo que G ++ no debería tener acceso a la información del tipo de tiempo de ejecución.
Al compilar el operador de delete
, el compilador necesita determinar una función ''desasignar'' para llamar después de que se ejecute el destructor. Tenga en cuenta que el destructor no tiene nada que ver directamente con la llamada de desasignación, pero tiene un efecto sobre cómo el compilador busca la función de desasignación.
En el caso habitual, no existe una función de desasignación de tipo específico para el objeto, en cuyo caso se utiliza la función de desasignación global y que siempre se declara implícitamente (C ++ 03 3.7.3 / 2):
void operator delete(void*) throw();
Tenga en cuenta que esta función ni siquiera toma un argumento de tamaño. Necesita determinar el tamaño de asignación basado en nada más que el valor del puntero. Eso se puede hacer almacenando el tamaño de la asignación justo antes de la dirección (¿hay alguna implementación que lo haga de otra manera?).
Sin embargo, antes de decidir utilizar esa función de desasignación, el compilador realiza una búsqueda para ver si se debe usar una función de desasignación específica de tipo. Esa función puede tener un solo parámetro (un void*
) o dos parámetros (un void*
y un size_t
).
Al buscar la función de desasignación, si el tipo estático del puntero utilizado como el operando para delete
tiene un destructor virtual, entonces (C ++ 03 12.5 / 4):
la función de desasignación es la que encuentra la búsqueda en la definición del destructor virtual del tipo dinámico
En efecto, cualquier función de desasignación de operator delete()
es virtual para los tipos que tienen un destructor virtual, aunque la función real debe ser static
(el estándar toma nota de esto en 12.5 / 7). En este caso, el compilador puede pasar el tamaño del objeto si es necesario porque tiene acceso al tipo dinámico del objeto (cualquier ajuste necesario para el puntero del objeto se puede encontrar de la misma manera).
Si el tipo estático del operando a delete
es estático, entonces la búsqueda de la función de desasignación del operator delete()
sigue las reglas habituales. Nuevamente, si el compilador selecciona una función de desasignación que necesita un parámetro de tamaño, puede hacerlo porque conoce el tipo estático del objeto en tiempo de compilación.
La situación final es aquella que da como resultado un comportamiento indefinido: si el tipo estático del puntero no tiene un destructor virtual sino que apunta a un objeto tipo derivado, entonces el compilador buscará potencialmente la función de desasignación incorrecta y pasará el tamaño incorrecto. Pero dado que es el resultado de un comportamiento indefinido, no importa.
Esto es claramente específico de la implementación. En la práctica, hay un número relativamente pequeño de formas sensatas de implementar cosas. Conceptualmente hay algunos problemas aquí:
Debe poder obtener un puntero al objeto más derivado, ese es el objeto que (conceptualmente) abarca a todos los otros tipos.
En C ++ estándar puedes hacer esto con un
dynamic_cast
:void *derrived = dynamic_cast<void*>(some_ptr);
Que obtiene la
C*
de solo unaB*
, por ejemplo:#include <iostream> struct A { unsigned a; virtual ~A() { } }; struct B { unsigned b; virtual ~B() { } }; struct C : public A, public B { unsigned c; }; int main() { C* c = new C; std::cout << static_cast<void*>(c) << "/n"; B* b = c; std::cout << static_cast<void*>(b) << "/n"; std::cout << dynamic_cast<void*>(b) << "/n"; delete b; }
Dio lo siguiente en mi sistema
0x912c008 0x912c010 0x912c008
Una vez hecho esto, se convierte en un problema de seguimiento de asignación de memoria estándar. Por lo general, esto se hace de una de estas dos formas: a) registra el tamaño de la asignación justo antes de la memoria asignada, encuentra que el tamaño es solo una resta del puntero o ob) registra asignaciones y libera memoria en una estructura de datos de algún tipo. Para más detalles, vea esta pregunta , que tiene una buena referencia.
Con glibc puede consultar el tamaño de una asignación dada con sensatez:
#include <iostream> #include <stdlib.h> #include <malloc.h> int main() { char *test = (char*)malloc(50); std::cout << malloc_usable_size(test) << "/n"; }
Esa información está disponible para ser liberada / eliminada de manera similar y se usa para averiguar qué hacer con el trozo de memoria devuelto.
Los detalles exactos de la implementación de malloc_useable_size
se dan en el código fuente de libc, en malloc / malloc.c:
(Lo siguiente incluye explicaciones ligeramente editadas por Colin Plumb).
Los trozos de memoria se mantienen usando un método de "etiqueta de límite" como se describe, por ejemplo, en Knuth o Standish. (Ver el artículo de Paul Wilson ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps para una encuesta de tales técnicas). Los tamaños de los trozos libres se almacenan tanto en el frente de cada trozo como en el fin. Esto hace que la consolidación de trozos fragmentados en trozos más grandes sea muy rápida. Los campos de tamaño también contienen bits que representan si los trozos son libres o están en uso.
Un trozo asignado se ve así:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of previous chunk, if allocated | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of chunk, in bytes |M|P| mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | User data starts here... . . . . (malloc_usable_size() bytes) . . | nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of chunk | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
La destrucción de un puntero de clase base requiere que haya implementado un destructor virtual. Si no lo hizo, todas las apuestas están apagadas.
El primer destructor llamado será el del objeto más derivado según lo determine el mecanismo virtual (vtable). ¡Este destructor conoce el tamaño del objeto! Puede capturar esa información en algún lugar, o pasarla por la cadena de destructores.
La implementación habitual (teóricamente, puede haber otras, dudo que haya en la práctica) es que hay un objeto vtable por base (si no, los objetos base no son polimorfos y no pueden usarse para eliminar). Ese vtable contiene no solo un puntero a funciones virtuales, sino también lo que se necesita para todo el RTTI, incluido el desplazamiento del objeto actual al más derivado.
Para explicar (probablemente haya diferencias en cualquier implementación real y puedo haber cometido algunos errores), esto es lo que realmente se usa:
struct A_VTable_Desc {
int offset;
void* (destructor)();
} AVTable = { 0, A::~A };
struct A_impl {
unsigned a;
A_VTable_Desc* vptr;
};
struct B_VTable_Desc {
int offset;
void* (destructor)();
} BVtable = { 0, &B::~B };
struct B_impl {
unsigned b;
B_VTable_Desc* __vptr;
};
A_VTable_Desc CAVtable = { 0, &C::~C_as_A };
B_VTable_Desc CBVtable = { -8, &C::~C_as_B };
struct C {
A_impl __aimpl;
B_impl __bimpl;
unsigned c;
};
y los constructores de C implícitamente hacen algo así como
this->__aimpl->__vptr = &CAVtable;
this->__bimpl->__vptr = &CBVtable;
Puede hacer esto de la misma manera que lo hace malloc. Algunos mallocs registran el tamaño justo antes del objeto en sí. La mayoría de los mallocs modernos son mucho más sofisticados. Vea tcmalloc , un asignador rápido que mantiene los objetos del mismo tamaño juntos en las páginas, de modo que solo necesita mantener la información de tamaño en una granularidad de página.
Se definió su implementación, pero una técnica de implementación común es que el destructor llama realmente a la operator delete
(en lugar del código con la delete
en ella), y hay un parámetro oculto para el destructor que controla si se llama al operator delete
.
Con esta implementación, la mayoría de las llamadas al destructor (todas las llamadas dtor explícitas, llamadas para variables automáticas y estáticas y llamadas a destructores base de destructores derivados) tendrán ese argumento oculto adicional en falso (para que no se llame al operador delete). Sin embargo, cuando hay una expresión de eliminación, llama al destructor de nivel superior para el objeto con el arg oculto verdadero. En su ejemplo, esto será C :: ~ C (), por lo que sabrá reclamar la memoria para todo el objeto
un puntero a un objeto polimórfico generalmente se implementa como un puntero al objeto y la tabla virtual, que contiene información sobre la clase subyacente del objeto. delete conocerá estos detalles de implementación y encontrará el destructor correcto