Por que o std :: shared_ptr <void> funciona

129

Encontrei algum código usando std :: shared_ptr para executar uma limpeza arbitrária no desligamento. No começo, achei que esse código não funcionaria, 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 fornece 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, conforme implementado para o 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 está correta?

E, é claro, a pergunta muito mais importante: isso garante o funcionamento do padrão ou pode mudar ainda mais os internos do std :: shared_ptr, outras implementações realmente quebram esse código?

LiKao
fonte
2
O que você esperava que acontecesse?
Lightness Races em órbita
1
Não há elenco lá - é uma conversão de shared_ptr <test> para shared_ptr <void>.
Alan Stokes
Para sua informação: aqui está o link para um artigo sobre std :: shared_ptr no MSDN: msdn.microsoft.com/en-us/library/bb982026.aspx e esta é a documentação do GCC: gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser

Respostas:

98

O truque é que std::shared_ptrexecuta o apagamento do tipo. Basicamente, quando um novo shared_ptré criado, ele armazena internamente uma deleterfunção (que pode ser fornecida como argumento ao construtor, mas, se não houver, o padrão é chamar delete). Quando o shared_ptré destruído, chama a função armazenada e chama o deleter.

Um esboço simples do apagamento de tipo simplificado com a função std :: e evitando toda a contagem de referências e outros problemas podem ser vistos 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 é repassado, de modo que, quando você constrói um shared_ptr<T>de, shared_ptr<U>as informações sobre o destruidor a chamar também são repassadas no deleter.

David Rodríguez - dribeas
fonte
Parece haver um erro de impressão: my_shared. Eu consertaria isso, mas não tenho privilégio de editar ainda.
Alexey Kukanov
@Alexey Kukanov, @Dennis Zickefoose: Obrigado pela edição, eu estava fora e não a vi.
David Rodríguez - dribeas
2
@ user102008 você não precisa de 'std :: function', mas é um pouco mais flexível (provavelmente não importa nada aqui), mas isso não muda o funcionamento do tipo apagamento, se você armazenar 'delete_deleter <T>' como o ponteiro de função 'void (void *)' que você está executando no tipo de apagamento: T saiu do tipo de ponteiro armazenado.
David Rodríguez - dribeas
1
Esse comportamento é garantido pelo padrão C ++, certo? Preciso de apagamento de tipo em uma das minhas classes e std::shared_ptr<void>evito declarar uma classe de invólucro inútil apenas para que eu possa herdá-la de uma determinada classe base.
Violet Giraffe
1
@AngelusMortis: o deleter exato não faz parte do tipo de my_unique_ptr. Quando maino modelo é instanciado, doubleo deleter correto é escolhido, mas isso não faz parte do tipo my_unique_ptre não pode ser recuperado do objeto. O tipo do deletador é apagado do objeto, quando uma função recebe uma my_unique_ptr(digamos, por referência de valor-r), essa função não sabe nem precisa saber o que é o deletador.
David Rodríguez - dribeas
35

shared_ptr<T> logicamente [*] possui (pelo menos) dois membros de dados relevantes:

  • um ponteiro para o objeto que está sendo gerenciado
  • um ponteiro para a função deleter que será usada para destruí-lo.

A função deleter da sua shared_ptr<Test>, dada a maneira como você a construiu, é a função normal para Test, que converte o ponteiro em Test*e deletes.

Quando você insere seu shared_ptr<Test>vetor shared_ptr<void>, ambos são copiados, embora o primeiro seja convertido em void*.

Portanto, quando o elemento vetorial é destruído levando a última referência, ele passa o ponteiro para um deleter que o destrói corretamente.

Na verdade, é um pouco mais complicado do que isso, porque shared_ptrpode levar um functor deleter em vez de apenas uma função; portanto, pode até haver dados por objeto a serem armazenados, em vez de apenas um ponteiro de função. Mas, nesse caso, não existem dados extras, seria suficiente armazenar um ponteiro na instanciação de uma função de modelo, com um parâmetro de modelo que captura o tipo pelo qual o ponteiro deve ser excluído.

[*] logicamente no sentido de ter acesso a eles - eles podem não ser membros do shared_ptr em si, mas em vez de algum nó de gerenciamento para o qual ele aponta.

Steve Jessop
fonte
2
+1 por mencionar que a função deleter / functor é copiada para outras instâncias shared_ptr - uma informação perdida em outras respostas.
Alexey Kukanov
Isso significa que os destruidores de base virtual não são necessários ao usar shared_ptrs?
21711 ronag
@ronag Sim. No entanto, eu ainda recomendaria tornar o destruidor virtual, pelo menos se você tiver outros membros virtuais. (A dor de acidentalmente esquecer uma vez supera qualquer possível benefício.)
Alan Stokes
Sim, eu concordo. Interessante não menos. Eu sabia que o apagamento de tipo simplesmente não havia considerado esse "recurso" dele.
21711 ronag
2
@ronag: destruidores virtuais não são necessários se você criar shared_ptrdiretamente com o tipo apropriado ou se você usar make_shared. Mas, ainda assim, é uma boa idéia como o tipo do ponteiro pode mudar de construção até que seja armazenada no shared_ptr: base *p = new derived; shared_ptr<base> sp(p);, na medida em que shared_ptrestá em causa o objeto é basenão derived, então você precisa de um destrutor virtual. Esse padrão pode ser comum aos padrões de fábrica, por exemplo.
David Rodríguez - dribeas
10

Funciona porque usa apagamento de tipo.

Basicamente, quando você cria um shared_ptr, ele passa um argumento extra (que você pode realmente fornecer, se desejar), que é o função deleter.

Esse functor padrão aceita como argumento um ponteiro para o tipo que você usa no shared_ptr, portanto, voidaqui, lança-o adequadamente no tipo estático que você usou testaqui e chama o destruidor neste objeto.

Qualquer ciência suficientemente avançada parece mágica, não é?

Matthieu M.
fonte
5

O construtor shared_ptr<T>(Y *p)realmente parece estar chamando shared_ptr<T>(Y *p, D d)where dé um deleter gerado automaticamente para o objeto.

Quando isso acontece, o tipo do objeto Yé conhecido; portanto, o deletador desse shared_ptrobjeto sabe qual destruidor chamar e essas informações não são perdidas quando o ponteiro é armazenado em um vetor de shared_ptr<void>.

De fato, as especificações exigem que, para que um shared_ptr<T>objeto recebedor aceite um shared_ptr<U>objeto, deve ser verdade que U*deve ser implicitamente convertível em ae T*este é certamente o caso, T=voidpois qualquer ponteiro pode ser convertido em um void*implicitamente. Nada é dito sobre o deleter que será inválido; portanto, as especificações exigem que isso funcione corretamente.

Tecnicamente, o IIRC a shared_ptr<T>mantém um ponteiro para um objeto oculto que contém o contador de referência e um ponteiro para o objeto real; armazenando o deleter nessa estrutura oculta, é possível fazer com que esse recurso aparentemente mágico funcione, mantendo shared_ptr<T>o tamanho de um ponteiro comum (no entanto, desmarcando o ponteiro requer uma dupla indireção

shared_ptr -> hidden_refcounted_object -> real_object
6502
fonte
3

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

Cachorro
fonte
Você pode explicar mais? Eu postei uma pergunta semelhante agora, seria ótimo se você pudesse ajudar!
Bruce
3

Vou responder a essa pergunta (2 anos depois) usando uma implementação muito simplista do shared_ptr que o usuário entenderá.

Em primeiro lugar, vou a algumas classes secundárias, shared_ptr_base, sp_counted_base sp_counted_impl, e verifiquei_determine o último dos quais é um modelo.

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 vou criar duas funções "gratuitas", 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 de modelo.

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 "test". Ele chamará make_sp_counted_impl () com um ponteiro para U, não um ponteiro para T. O gerenciamento da destruição é feito aqui. A classe shared_ptr_base gerencia a contagem de referência em relação à cópia e atribuição, etc. A própria classe shared_ptr gerencia o uso seguro de sobrecargas do operador (->, * etc).

Portanto, embora você tenha um shared_ptr para anular, abaixo você está gerenciando um ponteiro do tipo que passou para novo. Observe que, se você converter seu ponteiro em um vácuo * antes de colocá-lo no shared_ptr, ele falhará na compilação no selected_delete, para que você também esteja seguro lá.

CashCow
fonte