Por qué std:: shared ptr funciona


Encontré un 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 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 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 el funcionamiento interno de std::shared_ptrs implementado para G++. Dado que estos objetos envuelven el puntero interno junto con el contador, el molde de std::shared_ptr<test> a std::shared_ptr<void> probablemente no esté obstaculizando el llamada del destructor. ¿Es correcta esta suposición?

Y, por supuesto, la pregunta mucho más importante: ¿Está garantizado que esto funcione de acuerdo con el estándar, o podría haber cambios adicionales en el funcionamiento interno de std::shared_ptr, otras implementaciones realmente rompen este código?

Author: Zhen, 2011-05-06

6 answers

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

Se puede ver un esquema simple del tipo erasure que se está simplificando con std:: function, y evitando todo el conteo de referencias y otros problemas aquí:

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

 87
Author: David Rodríguez - dribeas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-26 14:55:22

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 deleter que se utilizará para destruirla.

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

Cuando empujas tu shared_ptr<Test> en el vector de shared_ptr<void>, ambos de ellos se copian, aunque el primero se convierte a void*.

Entonces, cuando el elemento vectorial es destruido tomando la última referencia con él, pasa el puntero a un deleter 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 solo una función, por lo que incluso podría haber datos por objeto para ser almacenados en lugar de solo un puntero de función. Pero para este caso no hay tales datos adicionales, sería suficiente solo para almacenar un puntero a una instanciación de una función de plantilla, con un parámetro de plantilla que captura el tipo a través del cual se debe eliminar el puntero.

[*] lógicamente en el sentido de que tiene acceso a ellos - pueden no ser miembros del mismo shared_ptr, sino en lugar de algún nodo de administración al que apunta.

 31
Author: Steve Jessop,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-06 15:43:15

Funciona porque utiliza el tipo erasure.

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

Este funtor por defecto acepta como argumento un puntero para escribir en el shared_ptr, por lo tanto void aquí, lo lanza apropiadamente al tipo estático que usó test aquí, y llama al destructor en este objeto.

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

 8
Author: Matthieu M.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-06 15:29:20

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

Cuando esto sucede, se conoce el tipo del objeto Y, por lo que el deleter para este objeto shared_ptr 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 objeto receving shared_ptr<T> acepte un objeto shared_ptr<U> debe ser cierto y U* debe ser implícitamente convertible a T* y este es ciertamente el caso con T=void porque cualquier puntero puede convertirse a void* implícitamente. No se dice nada sobre el deleter que será inválido, por lo que las especificaciones exigen que esto funcione correctamente.

Técnicamente IIRC a shared_ptr<T> sostiene un puntero a un objeto oculto que contiene el contador de referencia y un puntero al objeto real; almacenando el deleter en esta estructura oculta es posible hacer esta característica aparentemente mágica trabajando mientras se mantiene shared_ptr<T> tan grande como un puntero regular (sin embargo, desreferenciar el puntero requiere una doble indirección

shared_ptr -> hidden_refcounted_object -> real_object
 5
Author: 6502,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-06 15:58:56

Voy a responder esta pregunta (2 años después) usando una implementación muy simplista de shared_ptr que el usuario entenderá.

En primer lugar voy a algunas clases laterales, 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 "libres" 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 cree un shared_ptr a través de una función templada.

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 a través de aquí. La clase shared_ptr_base administra el conteo de referencias con respecto a la copia y asignación, etc. La clase shared_ptr gestiona el uso seguro de tipo de sobrecargas de operador ( - >, * sucesivamente).

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 a un void* antes de ponerlo en shared_ptr, fallará al compilar en checked_delete, por lo que también estará seguro allí.

 3
Author: CashCow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-15 09:45:23

Test* es implícitamente convertible a void*, por lo tanto shared_ptr<Test> es implícitamente convertible a shared_ptr<void>, de 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, utilizarán internamente la herencia para llamar al destructor apropiado como lo estaba en el momento de la asignación.

 2
Author: Puppy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-06 15:30:05