Encontré algo de código usando std :: shared_ptr para realizar una limpieza arbitraria al apagar. Al principio pensé que este código no podría funcionar, pero luego intenté lo siguiente:
#include <memory>
#include <iostream>
#include <vector>
class test {
public:
test() {
std::cout << "Test created" << std::endl;
}
~test() {
std::cout << "Test destroyed" << std::endl;
}
};
int main() {
std::cout << "At begin of main.\ncreating std::vector<std::shared_ptr<void>>"
<< std::endl;
std::vector<std::shared_ptr<void>> v;
{
std::cout << "Creating test" << std::endl;
v.push_back( std::shared_ptr<test>( new test() ) );
std::cout << "Leaving scope" << std::endl;
}
std::cout << "Leaving main" << std::endl;
return 0;
}
Este programa da la salida:
At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed
Tengo algunas ideas sobre por qué esto podría funcionar, que tienen que ver con las partes internas de std :: shared_ptrs implementadas para G ++. Dado que estos objetos se envuelven juntos el puntero interno con el contador del elenco de std::shared_ptr<test>
a std::shared_ptr<void>
probablemente no está obstaculizando la llamada del destructor. ¿Es correcta esta suposición?
Y, por supuesto, la pregunta mucho más importante: ¿está garantizado que esto funcione según el estándar, o podría haber más cambios en las partes internas de std :: shared_ptr, otras implementaciones realmente rompen este código?
fuente
Respuestas:
El truco es que
std::shared_ptr
realiza el borrado de tipo. Básicamente, cuandoshared_ptr
se crea una nueva , almacenará internamente unadeleter
función (que se puede dar como argumento para el constructor pero, si no está presente, los valores predeterminados para la llamadadelete
). Cuandoshared_ptr
se destruye, llama a esa función almacenada y eso llamará adeleter
.Aquí se puede ver un boceto simple del tipo de borrado que se está simplificando con std :: function, y evitando todo recuento de referencias y otros problemas:
Cuando
shared_ptr
se copia (o se construye por defecto) de otro, el eliminador se pasa, de modo que cuando se construye un ashared_ptr<T>
partir deshared_ptr<U>
la información sobre qué destructor llamar también se pasa en eldeleter
.fuente
my_shared
. Lo arreglaría, pero todavía no tengo el privilegio de editar.std::shared_ptr<void>
me permite evitar declarar una clase de contenedor inútil solo para poder heredarla de una determinada clase base.my_unique_ptr
. Cuandomain
sedouble
crea una instancia en la plantilla, se elige el eliminador correcto, pero esto no forma parte del tipo demy_unique_ptr
y no se puede recuperar del objeto. El tipo de borrador se borra del objeto, cuando una función recibe unmy_unique_ptr
(digamos por rvalue-reference), esa función no sabe y no necesita saber qué es el borrador.shared_ptr<T>
lógicamente [*] tiene (al menos) dos miembros de datos relevantes:La función de eliminación de su
shared_ptr<Test>
, dada la forma en que la construyó, es la normal paraTest
, que convierte el puntero enTest*
y paradelete
ello.Cuando se presiona el
shared_ptr<Test>
en el vector deshared_ptr<void>
, tanto de los que se copian, aunque el primero de ellos se convierte envoid*
.Entonces, cuando el elemento vector se destruye tomando la última referencia, pasa el puntero a un eliminador que lo destruye correctamente.
En realidad es un poco más complicado que esto, porque
shared_ptr
puede tomar un Deleter funtor en lugar de sólo una función, por lo que incluso podría ser datos de cada objeto a ser almacenados en lugar de sólo un puntero de función. Pero para este caso no hay tales datos adicionales, sería suficiente simplemente almacenar un puntero para una instanciación de una función de plantilla, con un parámetro de plantilla que capture el tipo a través del cual se debe eliminar el puntero.[*] lógicamente en el sentido de que tiene acceso a ellos; es posible que no sean miembros del shared_ptr en sí, sino que sean un nodo de administración al que apunta.
fuente
shared_ptr
directamente con el tipo apropiado o si lo usamake_shared
. Pero, aun así, es una buena idea, ya que el tipo de puntero puede cambiar desde la construcción hasta que se almacena en elshared_ptr
:,base *p = new derived; shared_ptr<base> sp(p);
en lo queshared_ptr
respecta al objeto ,base
noderived
lo es , por lo que necesita un destructor virtual. Este patrón puede ser común con los patrones de fábrica, por ejemplo.Funciona porque usa borrado de tipo.
Básicamente, cuando crea un
shared_ptr
, pasa un argumento adicional (que realmente puede proporcionar si lo desea), que es el functor de eliminación.Este functor predeterminado acepta como argumento un puntero para escribir que usa en el
shared_ptr
, por lo tanto,void
aquí, lo convierte adecuadamente al tipo estático que usótest
aquí, y llama al destructor en este objeto.Cualquier ciencia suficientemente avanzada se siente como magia, ¿no?
fuente
El constructor
shared_ptr<T>(Y *p)
de hecho parece estar llamandoshared_ptr<T>(Y *p, D d)
, donded
es un Deleter generada automáticamente para el objeto.Cuando esto sucede,
Y
se conoce el tipo de objeto , por lo que el eliminador de esteshared_ptr
objeto sabe a qué destructor llamar y esta información no se pierde cuando el puntero se almacena en un vector deshared_ptr<void>
.De hecho, las especificaciones requieren que para que un
shared_ptr<T>
objeto receptor acepte unshared_ptr<U>
objeto, debe ser cierto yU*
debe ser implícitamente convertible en aT*
y este es ciertamente el casoT=void
porque cualquier puntero puede convertirsevoid*
implícitamente. No se dice nada sobre el borrador que no sea válido, por lo que las especificaciones obligan a que funcione correctamente.Técnicamente, IIRC a
shared_ptr<T>
tiene un puntero a un objeto oculto que contiene el contador de referencia y un puntero al objeto real; Al almacenar el eliminador en esta estructura oculta, es posible hacer que esta característica aparentemente mágica funcione sin dejar de sershared_ptr<T>
tan grande como un puntero normal (sin embargo, desreferenciar el puntero requiere una doble indirecciónfuente
Test*
es implícitamente convertible avoid*
, porshared_ptr<Test>
lo tanto, es implícitamente convertible ashared_ptr<void>
, desde la memoria. Esto funciona porqueshared_ptr
está diseñado para controlar la destrucción en tiempo de ejecución, no en tiempo de compilación, usarán la herencia internamente para llamar al destructor apropiado como lo fue en el tiempo de asignación.fuente
Voy a responder esta pregunta (2 años después) usando una implementación muy simple de shared_ptr que el usuario entenderá.
En primer lugar, voy a algunas clases secundarias, shared_ptr_base, sp_counted_base sp_counted_impl, y Verified_deleter, la última de las cuales es una plantilla.
Ahora voy a crear dos funciones "gratuitas" llamadas make_sp_counted_impl que devolverán un puntero a uno recién creado.
Ok, estas dos funciones son esenciales en cuanto a lo que sucederá después cuando crees un shared_ptr a través de una función con plantilla.
Tenga en cuenta lo que sucede arriba si T es nulo y U es su clase de "prueba". Llamará a make_sp_counted_impl () con un puntero a U, no un puntero a T. La gestión de la destrucción se realiza aquí. La clase shared_ptr_base gestiona el recuento de referencias con respecto a la copia y la asignación, etc. La clase shared_ptr gestiona el uso seguro de las sobrecargas del operador (->, * etc.).
Por lo tanto, aunque tenga un shared_ptr para anular, debajo está administrando un puntero del tipo que pasó a nuevo. Tenga en cuenta que si convierte su puntero en un vacío * antes de ponerlo en shared_ptr, no se compilará en check_delete, por lo que también estará seguro allí.
fuente