¿Por qué funciona std :: shared_ptr <void>?

129

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?

LiKao
fuente
2
¿Qué esperabas que pasara en su lugar?
Carreras de ligereza en órbita
1
No hay conversión allí: es una conversión de shared_ptr <test> a shared_ptr <void>.
Alan Stokes
FYI: aquí está el enlace a un artículo sobre std :: shared_ptr en MSDN: msdn.microsoft.com/en-us/library/bb982026.aspx y esta es la documentación de GCC: gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser

Respuestas:

99

El truco es que std::shared_ptrrealiza el borrado de tipo. Básicamente, cuando shared_ptrse crea una nueva , almacenará internamente una deleterfunción (que se puede dar como argumento para el constructor pero, si no está presente, los valores predeterminados para la llamada delete). Cuando shared_ptrse destruye, llama a esa función almacenada y eso llamará a deleter.

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:

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 shared_ptrse copia (o se construye por defecto) de otro, el eliminador se pasa, de modo que cuando se construye un a shared_ptr<T>partir de shared_ptr<U>la información sobre qué destructor llamar también se pasa en el deleter.

David Rodríguez - dribeas
fuente
Parece que hay un error de imprenta: my_shared. Lo arreglaría, pero todavía no tengo el privilegio de editar.
Alexey Kukanov
@Alexey Kukanov, @Dennis Zickefoose: Gracias por la edición, estuve fuera y no lo vi.
David Rodríguez - dribeas
2
@ user102008 no necesita 'std :: function' pero es un poco más flexible (probablemente no importa aquí), pero eso no cambia la forma en que funciona el borrado de tipo, si almacena 'delete_deleter <T>' como el puntero de función 'void (void *)' está realizando el borrado de tipo allí: T ha desaparecido del tipo de puntero almacenado.
David Rodríguez - dribeas
1
Este comportamiento está garantizado por el estándar C ++, ¿verdad? Necesito borrar el tipo en una de mis clases, y std::shared_ptr<void>me permite evitar declarar una clase de contenedor inútil solo para poder heredarla de una determinada clase base.
Violet Giraffe
1
@AngelusMortis: El eliminador exacto no es parte del tipo de my_unique_ptr. Cuando mainse doublecrea una instancia en la plantilla, se elige el eliminador correcto, pero esto no forma parte del tipo de my_unique_ptry no se puede recuperar del objeto. El tipo de borrador se borra del objeto, cuando una función recibe un my_unique_ptr(digamos por rvalue-reference), esa función no sabe y no necesita saber qué es el borrador.
David Rodríguez - dribeas
35

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

  • un puntero al objeto que se administra
  • un puntero a la función de eliminación que se usará para destruirla.

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 en Test*y para deleteello.

Cuando se presiona el shared_ptr<Test>en el vector de shared_ptr<void>, tanto de los que se copian, aunque el primero de ellos se convierte en void*.

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_ptrpuede 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.

Steve Jessop
fuente
2
+1 por mencionar que la función / functor de eliminación se copia en otras instancias shared_ptr, una información que se perdió en otras respuestas.
Alexey Kukanov
¿Significa esto que no se necesitan destructores de bases virtuales cuando se usan shared_ptrs?
Ronag
@ronag Sí. Sin embargo, todavía recomendaría que el destructor sea virtual, al menos si tiene otros miembros virtuales. (El dolor de olvidar accidentalmente una vez supera cualquier beneficio posible.)
Alan Stokes
Sí, estaría de acuerdo. Interesante, no obstante. Sabía que el tipo de borrado simplemente no había considerado esta "característica".
Ronag
2
@ronag: los destructores virtuales no son necesarios si crea shared_ptrdirectamente con el tipo apropiado o si lo usa make_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 el shared_ptr:, base *p = new derived; shared_ptr<base> sp(p);en lo que shared_ptrrespecta al objeto , baseno derivedlo es , por lo que necesita un destructor virtual. Este patrón puede ser común con los patrones de fábrica, por ejemplo.
David Rodríguez - dribeas
10

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, voidaquí, lo convierte adecuadamente al tipo estático que usó testaquí, y llama al destructor en este objeto.

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

Matthieu M.
fuente
5

El constructor shared_ptr<T>(Y *p)de hecho parece estar llamando shared_ptr<T>(Y *p, D d), donde des un Deleter generada automáticamente para el objeto.

Cuando esto sucede, Yse conoce el tipo de objeto , por lo que el eliminador de este shared_ptrobjeto sabe a 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 que un shared_ptr<T>objeto receptor acepte un shared_ptr<U>objeto, debe ser cierto y U*debe ser implícitamente convertible en a T*y este es ciertamente el caso T=voidporque cualquier puntero puede convertirse void*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 ser shared_ptr<T>tan grande como un puntero normal (sin embargo, desreferenciar el puntero requiere una doble indirección

shared_ptr -> hidden_refcounted_object -> real_object
6502
fuente
3

Test*es implícitamente convertible a void*, por shared_ptr<Test>lo tanto, es implícitamente convertible a shared_ptr<void>, desde la memoria. Esto funciona porque shared_ptrestá 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.

Perrito
fuente
¿Puedes explicarme mas? He publicado una pregunta similar en este momento, ¡sería genial si pudieras ayudar!
Bruce
3

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.

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 lo que sucederá después cuando crees un shared_ptr a través de una función con 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 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í.

CashCow
fuente