web-dev-qa-db-fra.com

Pourquoi std :: shared_ptr <void> fonctionne

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

#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 la sortie:

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 pourquoi cela pourrait fonctionner, qui ont à voir avec les internes de std :: shared_ptrs comme implémenté pour G ++. Étant donné que ces objets enveloppent le pointeur interne avec le compteur, le plâtre de std::shared_ptr<test> à std::shared_ptr<void> n'entrave probablement pas l'appel du destructeur. Cette hypothèse est-elle correcte?

Et bien sûr, la question beaucoup plus importante: est-ce garanti de fonctionner selon la norme, ou pourrait-il y avoir d'autres changements dans les internes de std :: shared_ptr, d'autres implémentations cassent-elles réellement ce code?

122
LiKao

L'astuce est que std::shared_ptr efface le type. Fondamentalement, lorsqu'un nouveau shared_ptr est créé, il stockera en interne une fonction deleter (qui peut être donnée comme argument au constructeur mais si elle n'est pas présente par défaut pour appeler delete). Quand le shared_ptr est détruit, il appelle cette fonction stockée et qui appellera deleter.

Une simple esquisse du type d'effacement en cours simplifiée avec std :: function, et évitant tout comptage de références et autres problèmes peut être vue 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 construit par défaut) à partir d'un autre, le suppresseur est transmis, de sorte que lorsque vous construisez un shared_ptr<T> de shared_ptr<U> les informations sur le destructeur à appeler sont également transmises dans le deleter.

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

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

La fonction deleter de votre shared_ptr<Test>, étant donné la façon dont vous l'avez construit, est celui normal pour Test, qui convertit le pointeur en Test* et deletes.

Lorsque vous poussez votre shared_ptr<Test> dans le vecteur de shared_ptr<void>, les deux de ceux-ci sont copiés, bien que le premier soit converti en void*.

Ainsi, lorsque l'élément vectoriel est détruit en emportant la dernière référence avec lui, il passe le pointeur à un suppresseur qui le détruit correctement.

C'est en fait un peu plus compliqué que ça, parce que shared_ptr peut prendre un deleter functor plutôt qu'une simple fonction, donc il peut même y avoir des données par objet à stocker plutôt qu'un simple pointeur de fonction. Mais dans ce cas, il n'y a pas de telles données supplémentaires, il suffirait de stocker un pointeur vers une instanciation d'une fonction de modèle, avec un paramètre de modèle qui capture le type par lequel le pointeur doit être supprimé.

[*] logiquement dans le sens où il y a accès - ils peuvent ne pas être membres du shared_ptr lui-même mais au lieu d'un nœud de gestion vers lequel il pointe.

33
Steve Jessop

Cela fonctionne car il utilise l'effacement de type.

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

Ce foncteur par défaut accepte comme argument un pointeur à taper que vous utilisez dans le shared_ptr, donc void ici, le convertit de manière appropriée au type statique que vous avez utilisé test ici, et appelle le destructeur sur cet objet.

Toute science suffisamment avancée ressemble à de la magie, n'est-ce pas?

10
Matthieu M.

Le constructeur shared_ptr<T>(Y *p) semble en effet appeler shared_ptr<T>(Y *p, D d)d est un suppresseur généré automatiquement pour l'objet.

Lorsque cela se produit, le type de l'objet Y est connu, donc le suppresseur de ce shared_ptr l'objet sait quel destructeur appeler et cette information n'est pas perdue lorsque le pointeur est stocké dans un vecteur de shared_ptr<void>.

En effet les spécifications exigent que pour un receving shared_ptr<T> objet pour accepter un shared_ptr<U> objet il doit être vrai que et U* doit être implicitement convertible en T* et c'est certainement le cas avec T=void car tout pointeur peut être converti en void* implicitement. Rien n'est dit sur le deleter qui sera invalide, donc les spécifications exigent que cela fonctionne correctement.

Techniquement, l'IIRC a shared_ptr<T> contient un pointeur sur un objet caché qui contient le compteur de référence et un pointeur sur l'objet réel; en stockant le deleter dans cette structure cachée, il est possible de faire fonctionner cette fonctionnalité apparemment magique tout en conservant shared_ptr<T> aussi gros qu'un pointeur normal (cependant le déréférencement du pointeur nécessite une double indirection

shared_ptr -> hidden_refcounted_object -> real_object
5
6502

Test* est implicitement convertible en void*, donc shared_ptr<Test> est implicitement convertible en shared_ptr<void>, de mémoire. Cela fonctionne parce que shared_ptr est conçu pour contrôler la destruction au moment de l'exécution, et non à la compilation, ils utiliseront en interne l'héritage pour appeler le destructeur approprié comme il l'était au moment de l'allocation.

3
Puppy

Je vais répondre à cette question (2 ans plus tard) en utilisant une implémentation très simpliste de shared_ptr que l'utilisateur comprendra.

Tout d'abord, je vais passer à quelques classes secondaires, shared_ptr_base, sp_counted_base sp_counted_impl, et check_deleter dont la dernière 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; // 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
};

Maintenant, je vais créer deux fonctions "libres" appelées make_sp_counted_impl qui retourneront un pointeur sur une fonction nouvellement créée.

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 quant à ce qui se passera ensuite lorsque vous créez un shared_ptr via une fonction de modèle.

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 ci-dessus si T est nul et U est votre classe "test". Il appellera make_sp_counted_impl () avec un pointeur vers U, pas un pointeur vers T. La gestion de la destruction se fait ici. La classe shared_ptr_base gère le comptage des références en ce qui concerne la copie et l'affectation, etc. La classe shared_ptr elle-même gère l'utilisation sécurisée des surcharges d'opérateur (->, * etc).

Ainsi, bien que vous ayez un shared_ptr à annuler, vous gérez en dessous un pointeur du type que vous avez passé à new. Notez que si vous convertissez votre pointeur en un void * avant de le placer dans shared_ptr, il ne parviendra pas à se compiler sur le check_delete, vous êtes donc en sécurité là aussi.

3
CashCow