Pourquoi std :: shared_ptr <void> fonctionne-t-il

129

J'ai trouvé du code en 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 les raisons pour lesquelles cela pourrait fonctionner, qui ont à voir avec les composants internes de std :: shared_ptrs tels qu'implémentés pour G ++. Étant donné que ces objets encapsulent le pointeur interne avec le compteur, la conversion de std::shared_ptr<test>à std::shared_ptr<void>ne gêne probablement pas l'appel du destructeur. Cette hypothèse est-elle correcte?

Et bien sûr, la question beaucoup plus importante: est-ce que cela fonctionne avec la norme, ou des modifications supplémentaires apportées aux composants internes de std :: shared_ptr, d'autres implémentations peuvent-elles réellement casser ce code?

LiKao
la source
2
À quoi vous attendiez-vous à la place?
Courses de légèreté en orbite
1
Il n'y a pas de distribution ici - c'est une conversion de shared_ptr <test> en shared_ptr <void>.
Alan Stokes
FYI: voici le lien vers un article sur std :: shared_ptr dans MSDN: msdn.microsoft.com/en-us/library/bb982026.aspx et voici la documentation de GCC: gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser

Réponses:

99

L'astuce consiste à std::shared_ptreffectuer un effacement de type. Fondamentalement, quand un nouveau shared_ptrest créé, il stockera en interne une deleterfonction (qui peut être donnée comme argument au constructeur mais si elle n'est pas présente par défaut à l'appel delete). Lorsque le shared_ptrest détruit, il appelle cette fonction stockée et cela appellera ledeleter .

Une simple esquisse de l'effacement de type simplifié 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)

Quand a shared_ptrest copié (ou construit par défaut) à partir d'un autre, le suppresseur est transmis, de sorte que lorsque vous construisez a à shared_ptr<T>partir de a, shared_ptr<U>les informations sur le destructeur à appeler sont également transmises dans le fichier deleter.

David Rodríguez - Dribeas
la source
Il semble y avoir une erreur d' impression: my_shared. Je corrigerais cela mais je n'ai pas encore le privilège de modifier.
Alexey Kukanov
@Alexey Kukanov, @Dennis Zickefoose: Merci pour le montage, j'étais absent et je ne l'ai pas vu.
David Rodríguez - dribeas
2
@ user102008 vous n'avez pas besoin de 'std :: function' mais c'est un peu plus flexible (cela n'a probablement pas d'importance ici du tout), mais cela ne change pas le fonctionnement de l'effacement de type, si vous stockez 'delete_deleter <T>' comme le pointeur de fonction 'void (void *)' vous effectuez l'effacement de type là: T est parti du type de pointeur enregistré.
David Rodríguez - dribeas
1
Ce comportement est garanti par la norme C ++, non? J'ai besoin d'un effacement de type dans une de mes classes, et std::shared_ptr<void>me permet d'éviter de déclarer une classe wrapper inutile juste pour pouvoir l'hériter d'une certaine classe de base.
Violet Giraffe
1
@AngelusMortis: Le suppresseur exact ne fait pas partie du type de my_unique_ptr. Lorsque dans mainle modèle est instancié avec doublele bon suppresseur est choisi mais cela ne fait pas partie du type de my_unique_ptret ne peut pas être récupéré à partir de l'objet. Le type du suppresseur est effacé de l'objet, lorsqu'une fonction reçoit un my_unique_ptr(disons par rvalue-reference), cette fonction ne le sait pas et n'a pas besoin de savoir ce qu'est le suppresseur.
David Rodríguez - dribeas
35

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

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

La fonction de suppression de votre shared_ptr<Test>, étant donné la manière dont vous l'avez construite, est la fonction normale pour Test, qui convertit le pointeur en Test*etdelete s.

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

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

C'est en fait un peu plus compliqué que ça, car shared_ptrpeut prendre un foncteur de suppression 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 simplement 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.

Steve Jessop
la source
2
+1 pour avoir mentionné que la fonction / foncteur de suppression est copiée dans d'autres instances shared_ptr - une information manquée dans d'autres réponses.
Alexey Kukanov
Cela signifie-t-il que les destructeurs de base virtuels ne sont pas nécessaires lors de l'utilisation de shared_ptrs?
ronag
@ronag Oui. Cependant, je recommanderais toujours de rendre le destructeur virtuel, du moins si vous avez d'autres membres virtuels. (La douleur d'oublier accidentellement une fois l'emporte sur tout avantage possible.)
Alan Stokes
Oui, je serais d'accord. Intéressant non-le-moins. Je savais que l'effacement de caractères n'avait tout simplement pas envisagé cette «fonctionnalité».
ronag
2
@ronag: les destructeurs virtuels ne sont pas nécessaires si vous créez le shared_ptrdirectement avec le type approprié ou si vous utilisez make_shared. Mais, il est encore une bonne idée que le type du pointeur peut changer de construction jusqu'à ce qu'il soit stocké dans le shared_ptr: base *p = new derived; shared_ptr<base> sp(p);, en ce qui shared_ptrest concerne l'objet est basepas derived, vous avez donc besoin d' un destructeur virtuel. Ce modèle peut être commun aux modèles d'usine, par exemple.
David Rodríguez - dribeas
10

Cela fonctionne car il utilise l'effacement de type.

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

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

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

Matthieu M.
la source
5

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

Lorsque cela se produit, le type de l'objet Yest connu, ainsi le suppresseur de cet shared_ptrobjet 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 qu'un shared_ptr<T>objet recevant accepte un shared_ptr<U>objet, il doit être vrai que et U*doit être implicitement convertible en a T*et c'est certainement le cas avec T=voidparce que tout pointeur peut être converti en a void*implicitement. Rien n'est dit sur le suppresseur qui sera invalide, donc en effet, les spécifications exigent que cela fonctionnera correctement.

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

shared_ptr -> hidden_refcounted_object -> real_object
6502
la source
3

Test*est implicitement convertible en void*, donc shared_ptr<Test>implicitement convertible en shared_ptr<void>, depuis la mémoire. Cela fonctionne car il shared_ptrest conçu pour contrôler la destruction au moment de l'exécution, pas au moment de la compilation, ils utiliseront en interne l'héritage pour appeler le destructeur approprié tel qu'il était au moment de l'allocation.

Chiot
la source
Pouvez-vous expliquer plus? J'ai posté une question similaire tout à l'heure, ce serait formidable si vous pouviez aider!
Bruce
3

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 à quelques classes secondaires, shared_ptr_base, sp_counted_base sp_counted_impl et checked_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 "gratuites" appelées make_sp_counted_impl qui renverront un pointeur vers 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 pour ce qui va se passer ensuite lorsque vous créez un shared_ptr via une fonction basée sur un 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 par 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 de type sécurisé des surcharges d'opérateurs (->, * etc).

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

Vache à lait
la source