c++ - weak_ptr - Por que o std:: shared_ptr<void> funciona




weak_ptr example c++ (4)

Eu encontrei algum código usando std :: shared_ptr para executar a limpeza arbitrária no desligamento. No começo eu pensei que esse código não poderia funcionar, mas tentei o seguinte:

#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 dá a saída:

At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed

Eu tenho algumas idéias sobre por que isso pode funcionar, que tem a ver com os internos de std :: shared_ptrs como implementado para G + +. Como esses objetos envolvem o ponteiro interno junto com o contador, a conversão de std::shared_ptr<test> para std::shared_ptr<void> provavelmente não está atrapalhando a chamada do destruidor. Esta suposição é correta?

E, claro, a questão muito mais importante: isso é garantido para trabalhar pelo padrão, ou pode ainda mais alterações para os internos de std :: shared_ptr, outras implementações realmente quebrar esse código?


Eu vou responder a esta pergunta (2 anos depois) usando uma implementação simplista de shared_ptr que o usuário entenderá.

Primeiramente, vou a algumas classes secundárias, shared_ptr_base, sp_counted_base sp_counted_impl e checked_deleter, a última das quais é um template.

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
};

Agora eu vou criar duas funções "livres" chamadas make_sp_counted_impl que retornarão um ponteiro para um recém-criado.

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, essas duas funções são essenciais para o que acontecerá a seguir quando você criar um shared_ptr por meio de uma função modelada.

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->
};

Observe o que acontece acima se T for nulo e U for sua classe "teste". Ele chamará make_sp_counted_impl () com um ponteiro para U, não um ponteiro para T. O gerenciamento da destruição é feito por aqui. A classe shared_ptr_base gerencia a contagem de referência no que diz respeito à cópia e atribuição etc. A própria classe shared_ptr gerencia o uso seguro de sobrecargas do operador (->, * etc).

Assim, embora você tenha um shared_ptr para anular, abaixo você está gerenciando um ponteiro do tipo que você passou para o novo. Note que se você converter seu ponteiro para um void * antes de colocá-lo no shared_ptr, ele irá falhar em compilar no checked_delete, então você também estará seguro lá.


Funciona porque usa o tipo de apagamento.

Basicamente, quando você constrói um shared_ptr , ele passa um argumento extra (que você pode realmente fornecer se desejar), que é o functor deleter.

Este functor padrão aceita como argumento um ponteiro para digitar que você usa no shared_ptr , portanto void aqui, converta apropriadamente para o tipo estático usado aqui e chame o destruidor neste objeto.

Qualquer ciência suficientemente avançada parece magia, não é?


O truque é que std::shared_ptr executa o apagamento de tipos. Basicamente, quando um novo shared_ptr é criado, ele armazena internamente uma função deleter (que pode ser dada como argumento para o construtor, mas se não estiver presente, o padrão é chamar delete ). Quando o shared_ptr é destruído, ele chama a função armazenada e chama o deleter .

Um esboço simples do tipo de eliminação que está sendo simplificado com std :: function e evitando toda contagem de referência e outros problemas pode ser visto aqui:

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)

Quando um shared_ptr é copiado (ou construído por padrão) de outro, o deleter é passado, de forma que quando você constrói um shared_ptr<T> partir de um shared_ptr<U> a informação sobre qual destruidor chamar também é passada pelo deleter .


Test* é implicitamente conversível para void* , portanto, shared_ptr<Test> é implicitamente conversível para shared_ptr<void> , da memória. Isso funciona porque shared_ptr é projetado para controlar a destruição em tempo de execução, não em tempo de compilação, eles usarão a herança internamente para chamar o destruidor apropriado como era no momento da alocação.





shared-ptr