126 votes

Pourquoi ne std::shared_ptr<void> travailler</void>

J'ai trouvé un code en utilisant std::shared_ptr pour effectuer arbitraire de nettoyage à l'arrêt. Au début, je pensais que ce code ne pouvait pas travailler, mais ensuite j'ai essayé le suivant:

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

Ce programme donne le résultat:

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

J'ai quelques idées sur les raisons de ce qui pourrait fonctionner, qui ont à voir avec le fonctionnement interne de std::shared_ptrs mis en œuvre pour G++. Depuis ces objets envelopper le pointeur interne en collaboration avec le comptoir de la distribution de std::shared_ptr<test> de std::shared_ptr<void> n'est probablement pas entraver l'appel du destructeur. Cette hypothèse est correcte?

Et bien sûr le plus important de la question: Est-ce garanti par la norme, ou peut-autres changements à l'intérieur de std::shared_ptr, d'autres implémentations de casser ce code?

95voto

Le truc, c'est qu' std::shared_ptr effectue ce type d'effacement. En gros, quand un nouveau shared_ptr est créé, il va stocker en interne une deleter de la fonction (qui peut être donné en argument au constructeur, mais si pas présent par défaut en appelant delete). Lorsque l' shared_ptr sont détruits, les appels de fonction en mémoire et qui va appeler l' deleter.

Un simple croquis du type d'effacement qui se passe simplifié avec std::function, et en évitant tous de comptage de référence et d'autres questions peut être vu ici:

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)

Lorsqu'un shared_ptr est copié (ou par défaut construit) à partir d'un autre la deleter est passé autour, de sorte que lorsque vous construisez un shared_ptr<T> d'un shared_ptr<U> les informations sur ce destructeur d'appel est également passé autour de l' deleter.

35voto

Steve Jessop Points 166970

shared_ptr<T> logiquement[*] a (au moins) deux membres de données:

  • un pointeur vers l'objet géré
  • un pointeur vers la deleter fonction qui sera utilisée pour la détruire.

La deleter fonction de votre shared_ptr<Test>, compte tenu de la façon dont vous avez construit, est la procédure normale pour l' Test, qui convertit le pointeur à l' Test* et deletes il.

Lorsque vous poussez votre shared_ptr<Test> dans le vecteur d' shared_ptr<void>, à la fois de ceux qui sont copiés, même si le premier est converti void*.

Ainsi, lorsque l'élément du vecteur est détruit en prenant la dernière référence, il passe le pointeur à un deleter, qui détruit correctement.

C'est effectivement un peu plus compliqué que cela, car shared_ptr pouvez prendre une deleter foncteur plutôt que de simplement une fonction, donc il y aura peut-être même par objet de stocker des données plutôt que de simplement un pointeur de fonction. Mais pour ce cas, il n'existe pas de données supplémentaires, il serait suffisant pour stocker un pointeur vers une instanciation d'un modèle de fonction, avec un paramètre de modèle qui capture le type à travers lequel le pointeur doit être supprimé.

[*] logiquement dans le sens qu'il a accès à eux - ils ne peuvent être membres de la shared_ptr lui-même mais plutôt de la gestion du nœud vers lequel il pointe.

10voto

Matthieu M. Points 101624

Cela fonctionne, car il utilise le type d'effacement.

Fondamentalement, lorsque vous générez un shared_ptr, il passe un argument supplémentaire (que vous pouvez réellement fournir si vous le souhaitez), qui est la deleter foncteur.

Ce défaut foncteur accepte comme argument un pointeur vers le type que vous utilisez dans l' shared_ptr, ainsi void ici, il jette de manière appropriée pour le type statique que vous avez utilisé test ici, et appelle le destructeur sur cet objet.

Tout suffisamment avancée de la science se sent comme de la magie, n'est-ce pas ?

5voto

6502 Points 42700

Le constructeur shared_ptr<T>(Y *p) , en fait, semble être appelant shared_ptr<T>(Y *p, D d)d est générée automatiquement deleter pour l'objet.

Lorsque cela arrive, le type de l'objet Y est connu, de sorte que le deleter pour cette shared_ptr objet sait quel destructeur d'appel et cette information n'est pas perdue lorsque le pointeur est stocké dans un vecteur d' shared_ptr<void>.

En effet, les spécifications exigent que, pour un receving shared_ptr<T> objet à accepter un shared_ptr<U> objet qu'il doit être vrai que et U* doit être implicitement converti à un T* , et c'est certainement le cas avec T=void car un pointeur peut être converti en void* implicitement. Rien n'est dit à propos de la deleter, qui sera invalide si, en effet, que les spécifications sont exigeant que cela fonctionnera correctement.

Techniquement IIRC un shared_ptr<T> détient un pointeur vers un objet caché qui contient le compteur de référence et un pointeur de l'objet réel; par le stockage de la deleter dans cette structure cachée, il est possible de faire ce qui, apparemment, la magie de la fonctionnalité de travail tout en gardant shared_ptr<T> gros comme un pointeur normal (cependant déréférencement du pointeur exige une double indirection

shared_ptr -> hidden_refcounted_object -> real_object

3voto

CashCow Points 18388

Je vais répondre à cette question (2 ans plus tard) à l'aide d'un très simpliste de la mise en œuvre de shared_ptr que l'utilisateur puisse comprendre.

Tout d'abord je vais un peu de côté les classes, shared_ptr_base, sp_counted_base sp_counted_impl, et checked_deleter dont le dernier est un modèle.

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

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

Maintenant, je vais créer deux "libre" fonction appelée make_sp_counted_impl qui renvoie un pointeur vers une nouvelle.

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, ces deux fonctions sont essentielles à ce qui va se passer lorsque vous créez un shared_ptr à travers basé sur un modèle de fonction.

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

Notez ce qui se passe au-dessus de si T est vide, et U est votre "test" de la classe. Il va appeler make_sp_counted_impl() avec un pointeur vers U, pas un pointeur vers T. La gestion de la destruction se fait par ici. Le shared_ptr_base classe gère le comptage de référence en ce qui concerne la copie et l'affectation etc. Le shared_ptr classe gère elle-même la typesafe utilisation de la surcharge de l'opérateur (->, * etc).

Ainsi, même si vous avez un shared_ptr à vide, au-dessous de vous sont la gestion d'un pointeur du type que vous avez passé dans les nouvelles. Notez que si vous convertissez votre pointeur void* avant de les mettre dans le shared_ptr, il ne pourra pas compiler sur le checked_delete de sorte que vous êtes réellement en sécurité là-bas aussi.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X