weak_ptr smart shared_ptr pointer example create c++ c++11 shared-ptr

shared_ptr - smart pointers c++



¿Por qué std:: shared_ptr<void> work (6)

El constructor shared_ptr<T>(Y *p) parece estar llamando a shared_ptr<T>(Y *p, D d) donde d es un eliminador generado automáticamente para el objeto.

Cuando esto ocurre, se conoce el tipo de objeto Y , por lo que el eliminador de este objeto shared_ptr sabe qué destructor llamar y esta información no se pierde cuando el puntero se almacena en un vector de shared_ptr<void> .

De hecho, las especificaciones requieren que para recibir un objeto shared_ptr<T> para aceptar un objeto shared_ptr<U> debe ser cierto que y U* debe ser implícitamente convertible a T* y este es ciertamente el caso con T=void porque cualquier puntero se puede convertir a un void* implícitamente. No se dice nada sobre el eliminador que no será válido, así que las especificaciones obligan a que esto funcione correctamente.

Técnicamente IIRC un shared_ptr<T> contiene 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 funcionar esta característica aparentemente mágica mientras se mantiene shared_ptr<T> tan grande como un puntero regular (sin embargo, eliminar la referencia del puntero requiere una doble indirección

shared_ptr -> hidden_refcounted_object -> real_object

Encontré un código usando std :: shared_ptr para realizar una limpieza arbitraria en el cierre. Al principio pensé que este código no podría funcionar, pero luego probé 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 el resultado:

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 como se implementó para G ++. Como estos objetos envuelven el puntero interno junto con el contador, la std::shared_ptr<test> de std::shared_ptr<test> a std::shared_ptr<void> probablemente no obstaculice la invocación del destructor. ¿Es correcta esta suposición?

Y, por supuesto, la pregunta mucho más importante: ¿está garantizado que esto funcionará 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?


El truco es que std::shared_ptr realiza borrado de tipo. Básicamente, cuando se crea un nuevo shared_ptr , se almacenará internamente una función de deleter (que se puede dar como argumento al constructor, pero si no está presente de manera predeterminada se llama a delete ). Cuando se destruye el shared_ptr , llama a esa función almacenada y eso llamará al deleter .

Aquí se puede ver un simple boceto del borrado de tipo simplificado con std :: function y evitar el recuento de referencias y otros problemas.

template <typename T> void delete_deleter( void * p ) { delete static_cast<T*>(p); } template <typename T> class my_unique_ptr { std::function< void (void*) > deleter; T * p; template <typename U> my_unique_ptr( U * p, std::function< void(void*) > deleter = &delete_deleter<U> ) : p(p), deleter(deleter) {} ~my_unique_ptr() { deleter( p ); } }; int main() { my_unique_ptr<void> p( new double ); // deleter == &delete_deleter<double> } // ~my_unique_ptr calls delete_deleter<double>(p)

Cuando se copia un shared_ptr (o construido por defecto) de otro, se pasa el eliminador, de modo que cuando se construye un shared_ptr<T> desde un shared_ptr<U> la información sobre qué destructor llamar también se pasa en el deleter .


Funciona porque usa borrado de tipo.

Básicamente, cuando construyes un shared_ptr , pasa un argumento extra (que puedes proporcionar si lo deseas), que es el functor deleter.

Este funtor por defecto acepta como argumento un puntero al tipo que usa en el shared_ptr , por lo tanto, es void aquí, lo relaciona apropiadamente con el tipo estático que utilizó aquí y llama al destructor sobre este objeto.

Cualquier ciencia suficientemente avanzada se siente como magia, ¿no es así?


Voy a responder esta pregunta (2 años después) utilizando 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 checked_deleter, la última de las cuales es una plantilla.

class sp_counted_base { public: sp_counted_base() : refCount( 1 ) { } virtual ~sp_deleter_base() {}; virtual void destruct() = 0; void incref(); // increases reference count void decref(); // decreases refCount atomically and calls destruct if it hits zero private: long refCount; // in a real implementation use an atomic int }; template< typename T > class sp_counted_impl : public sp_counted_base { public: typedef function< void( T* ) > func_type; void destruct() { func(ptr); // or is it (*func)(ptr); ? delete this; // self-destructs after destroying its pointer } template< typename F > sp_counted_impl( T* t, F f ) : ptr( t ), func( f ) private: T* ptr; func_type func; }; template< typename T > struct checked_deleter { public: template< typename T > operator()( T* t ) { size_t z = sizeof( T ); delete t; } }; class shared_ptr_base { private: sp_counted_base * counter; protected: shared_ptr_base() : counter( 0 ) {} explicit shared_ptr_base( sp_counter_base * c ) : counter( c ) {} ~shared_ptr_base() { if( counter ) counter->decref(); } shared_ptr_base( shared_ptr_base const& other ) : counter( other.counter ) { if( counter ) counter->addref(); } shared_ptr_base& operator=( shared_ptr_base& const other ) { shared_ptr_base temp( other ); std::swap( counter, temp.counter ); } // other methods such as reset };

Ahora voy a crear dos funciones "gratuitas" llamadas make_sp_counted_impl que devolverán un puntero a uno recién creado.

template< typename T, typename F > sp_counted_impl<T> * make_sp_counted_impl( T* ptr, F func ) { try { return new sp_counted_impl( ptr, func ); } catch( ... ) // in case the new above fails { func( ptr ); // we have to clean up the pointer now and rethrow throw; } } template< typename T > sp_counted_impl<T> * make_sp_counted_impl( T* ptr ) { return make_sp_counted_impl( ptr, checked_deleter<T>() ); }

Ok, estas dos funciones son esenciales en cuanto a qué sucederá a continuación cuando creas un shared_ptr a través de una función de plantilla.

template< typename T > class shared_ptr : public shared_ptr_base { public: template < typename U > explicit shared_ptr( U * ptr ) : shared_ptr_base( make_sp_counted_impl( ptr ) ) { } // implement the rest of shared_ptr, e.g. operator*, operator-> };

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 a un puntero a T. La gestión de la destrucción se realiza aquí. La clase shared_ptr_base gestiona el recuento de referencias en lo que respecta a la copia y la asignación, etc. La clase shared_ptr en sí administra el uso seguro de las sobrecargas del operador (->, * etc).

Por lo tanto, aunque tenga un shared_ptr to void, debajo usted maneja un puntero del tipo que pasó a nuevo. Tenga en cuenta que si convierte su puntero a un vacío * antes de colocarlo en el shared_ptr, no podrá compilarse en el check_delete por lo que también estará a salvo allí.


Test* es implícitamente convertible a void* , por shared_ptr<Test> tanto shared_ptr<Test> es implícitamente convertible a shared_ptr<void> , desde la memoria. Esto funciona porque shared_ptr está diseñado para controlar la destrucción en tiempo de ejecución, no en tiempo de compilación, usarán internamente la herencia para llamar al destructor apropiado tal como estaba en el momento de la asignación.


shared_ptr<T> lógicamente [*] tiene (al menos) dos miembros de datos relevantes:

  • un puntero al objeto que se está gestionando
  • un puntero a la función de eliminación que se usará para destruirlo.

La función de eliminación de su shared_ptr<Test> , dada la forma en que la construyó, es la normal para Test , que convierte el puntero a Test* y lo delete .

Cuando shared_ptr<Test> su shared_ptr<Test> en el vector de shared_ptr<void> , ambos se copian, aunque el primero se convierte en void* .

Entonces, cuando el elemento vector se destruye tomando la última referencia con él, 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 functor de eliminación en lugar de solo una función, por lo que incluso podría haber datos por objeto para almacenar en lugar de simplemente un puntero a la función. Pero para este caso no hay datos adicionales, bastaría con almacenar un puntero a una instancia 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 de algún nodo de gestión al que apunta.