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.