Perché std :: shared_ptr <void> funziona


129

Ho trovato del codice usando std :: shared_ptr per eseguire una pulizia arbitraria allo spegnimento. Inizialmente pensavo che questo codice non potesse funzionare, ma poi ho provato quanto segue:

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

Questo programma fornisce l'output:

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

Ho alcune idee sul perché questo potrebbe funzionare, che hanno a che fare con gli interni di std :: shared_ptrs implementati per G ++. Dal momento che questi oggetti avvolgono il puntatore insieme interno con il contatore il cast da std::shared_ptr<test>a std::shared_ptr<void>non è probabilmente ostacolare la chiamata del distruttore. Questo assunto è corretto?

E, naturalmente, la domanda molto più importante: questo è garantito per funzionare dallo standard o potrebbe cambiare ulteriormente gli interni di std :: shared_ptr, altre implementazioni in realtà infrangono questo codice?


2
Cosa ti aspettavi che succedesse invece?
Razze di leggerezza in orbita,

1
Non c'è cast qui - è una conversione da shared_ptr <test> a shared_ptr <void>.
Alan Stokes,

Cordiali saluti: ecco il link ad un articolo su std :: shared_ptr in MSDN: msdn.microsoft.com/en-us/library/bb982026.aspx e questa è la documentazione di GCC: gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser

Risposte:


98

Il trucco è che std::shared_ptresegue la cancellazione del tipo. Fondamentalmente, quando shared_ptrviene creato un nuovo, esso memorizzerà internamente una deleterfunzione (che può essere fornita come argomento al costruttore ma se non presenta valori predefiniti per la chiamata delete). Quando shared_ptrviene distrutto, chiama quella funzione memorizzata e quella chiamerà deleter.

Un semplice schizzo del tipo di cancellazione che è in corso semplificato con std :: function, ed evitando tutti i conteggi dei riferimenti e altri problemi, può essere visto qui:

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 un shared_ptrviene copiato (o costruito per impostazione predefinita) da un altro, il deleter viene passato in giro, in modo che quando si costruisce un shared_ptr<T>da un shared_ptr<U>le informazioni su quale distruttore chiamare vengano anche passate nel deleter.


Sembra che ci sia un errore di stampa: my_shared. Lo risolverei ma non ho ancora il privilegio di modificarlo.
Alexey Kukanov,

@Alexey Kukanov, @Dennis Zickefoose: Grazie per la modifica ero via e non l'ho visto.
David Rodríguez - dribeas

2
@ user102008 non hai bisogno di 'std :: function' ma è un po 'più flessibile (probabilmente non importa affatto qui), ma ciò non cambia il modo in cui funziona la cancellazione del tipo, se memorizzi' delete_deleter <T> 'come il puntatore a funzione 'void (void *)' su cui si sta eseguendo la cancellazione del tipo lì: T è passato dal tipo di puntatore memorizzato.
David Rodríguez - dribeas,

1
Questo comportamento è garantito dallo standard C ++, giusto? Ho bisogno della cancellazione del tipo in una delle mie classi e std::shared_ptr<void>mi consente di evitare di dichiarare una classe wrapper inutile solo per poter ereditarla da una determinata classe base.
Violet Giraffe,

1
@AngelusMortis: il deleter esatto non fa parte del tipo di my_unique_ptr. Quando nel mainmodello viene istanziato con doubleil giusto deleter viene scelto ma questo non fa parte del tipo di my_unique_ptre non può essere recuperato dall'oggetto. Il tipo di deleter viene cancellato dall'oggetto, quando una funzione riceve un my_unique_ptr(diciamo per rvalue-reference), quella funzione non lo fa e non ha bisogno di sapere quale sia il deleter.
David Rodríguez - dribeas,

35

shared_ptr<T> logicamente [*] ha (almeno) due membri di dati rilevanti:

  • un puntatore all'oggetto gestito
  • un puntatore alla funzione deleter che verrà utilizzata per distruggerla.

La tua funzione deleter shared_ptr<Test>, data la maniera in cui l'hai costruita, è quella normale per Test, che converte il puntatore in Test*e deletes.

Quando spingi il tuo shared_ptr<Test>nel vettore di shared_ptr<void>, entrambi vengono copiati, anche se il primo viene convertito in void*.

Quindi, quando l'elemento vettoriale viene distrutto portando con sé l'ultimo riferimento, passa il puntatore a un deleter che lo distrugge correttamente.

In realtà è un po 'più complicato di così, perché shared_ptrpuò richiedere un deleter functor piuttosto che solo una funzione, quindi potrebbero anche essere archiviati dati per oggetto piuttosto che solo un puntatore a funzione. Ma per questo caso non ci sono tali dati extra, sarebbe sufficiente solo memorizzare un puntatore a un'istanza di una funzione modello, con un parametro modello che acquisisce il tipo attraverso il quale il puntatore deve essere eliminato.

[*] logicamente nel senso che ha accesso a loro - potrebbero non essere membri di shared_ptr stesso ma invece di un nodo di gestione a cui punta.


2
+1 per menzionare che la funzione / funzione deleter viene copiata in altre istanze shared_ptr - un'informazione mancata in altre risposte.
Alexey Kukanov,

Questo significa che i distruttori di base virtuali non sono necessari quando si usano shared_ptrs?
ronag,

@ronag Sì. Tuttavia, consiglierei comunque di rendere virtuale il distruttore, almeno se hai altri membri virtuali. (Il dolore di dimenticare per caso una volta supera ogni possibile beneficio.)
Alan Stokes,

Sì, sono d'accordo. Interessante comunque. Sapevo che la cancellazione dei tipi non aveva preso in considerazione questa "caratteristica".
ronag,

2
@ronag: i distruttori virtuali non sono richiesti se si crea shared_ptrdirettamente con il tipo appropriato o se si utilizza make_shared. Ma, comunque, è una buona idea come il tipo del puntatore può cambiare da costruzione fino a quando non viene memorizzato nel shared_ptr: base *p = new derived; shared_ptr<base> sp(p);, per quanto shared_ptrconcerne l'oggetto è basenon è derived, quindi è necessario un distruttore virtuale. Questo modello può essere comune con i modelli di fabbrica, ad esempio.
David Rodríguez - dribeas

10

Funziona perché utilizza la cancellazione del tipo.

Fondamentalmente, quando costruisci un shared_ptr, passa un ulteriore argomento (che puoi effettivamente fornire se lo desideri), che è il deleter functor.

Questo functor predefinito accetta come argomento un puntatore da digitare nel shared_ptr, quindi voidqui lo lancia in modo appropriato al tipo statico che hai usato testqui e chiama il distruttore su questo oggetto.

Qualsiasi scienza sufficientemente avanzata sembra magica, no?


5

Il costruttore shared_ptr<T>(Y *p)sembra infatti chiamare shared_ptr<T>(Y *p, D d)dove si dtrova un deleter generato automaticamente per l'oggetto.

Quando ciò accade, il tipo di oggetto Yè noto, quindi il deleter per questo shared_ptroggetto sa quale distruttore chiamare e queste informazioni non vanno perse quando il puntatore è memorizzato in un vettore di shared_ptr<void>.

In effetti, le specifiche richiedono che per un shared_ptr<T>oggetto in attesa di accettare un shared_ptr<U>oggetto sia vero che U*deve essere implicitamente convertibile in a T*e questo è certamente il caso T=voidperché qualsiasi puntatore può essere convertito in modo void*implicito. Non si dice nulla sul deleter che non sarà valido, quindi le specifiche obbligano a farlo funzionare correttamente.

Tecnicamente IIRC a shared_ptr<T>tiene un puntatore a un oggetto nascosto che contiene il contatore di riferimento e un puntatore all'oggetto reale; memorizzando il deleter in questa struttura nascosta è possibile far funzionare questa funzionalità apparentemente magica mantenendo comunque shared_ptr<T>grande quanto un puntatore normale (tuttavia il dereferenziamento del puntatore richiede una doppia indiretta

shared_ptr -> hidden_refcounted_object -> real_object

3

Test*è implicitamente convertibile in void*, quindi shared_ptr<Test>è implicitamente convertibile in shared_ptr<void>, dalla memoria. Questo perché shared_ptrprogettato per controllare la distruzione in fase di esecuzione, non in fase di compilazione, utilizzeranno internamente l'ereditarietà per chiamare il distruttore appropriato com'era al momento dell'allocazione.


Puoi spiegare di più? Ho pubblicato una domanda simile proprio ora, sarebbe bello se tu potessi aiutare!
Bruce,

3

Risponderò a questa domanda (2 anni dopo) usando un'implementazione molto semplicistica di shared_ptr che l'utente capirà.

In primo luogo vado ad alcune classi secondarie, shared_ptr_base, sp_counted_base sp_counted_impl e controllato_deleter l'ultimo dei quali è un modello.

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

Ora ho intenzione di creare due funzioni "gratuite" chiamate make_sp_counted_impl che restituiranno un puntatore a uno appena creato.

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, queste due funzioni sono essenziali per quello che succederà dopo quando crei shared_ptr attraverso una funzione basata su modelli.

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

Nota cosa succede sopra se T è nullo e U è la tua classe "test". Chiamerà make_sp_counted_impl () con un puntatore a U, non un puntatore a T. La gestione della distruzione è fatta da qui. La classe shared_ptr_base gestisce il conteggio dei riferimenti per quanto riguarda la copia e l'assegnazione, ecc. La stessa classe shared_ptr gestisce l'uso tipico dei sovraccarichi dell'operatore (->, * ecc.).

Quindi anche se hai un shared_ptr da annullare, sotto stai gestendo un puntatore del tipo che hai passato in nuovo. Si noti che se si converte il puntatore in un vuoto * prima di inserirlo in shared_ptr, non verrà compilato su checked_delete in modo da essere effettivamente al sicuro anche lì.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.