web-dev-qa-db-fra.com

Comment le std :: tr1 :: shared_ptr est-il implémenté?

J'ai pensé à utiliser des pointeurs partagés, et je sais comment en implémenter un moi-même - Je ne veux pas le faire, alors j'essaie std::tr1::shared_ptr, et j'ai quelques questions ...

Comment le comptage des références est-il mis en œuvre? Utilise-t-il une liste doublement liée? (Btw, j'ai déjà googlé, mais je ne trouve rien de fiable.)

Y a-t-il des pièges à utiliser avec le std::tr1::shared_ptr?

35
purepureluck

shared_ptr Doit gérer un compteur de référence et le portage d'un foncteur deleter déduit par le type d'objet donné lors de l'initialisation.

La classe shared_ptr Héberge généralement deux membres: un T* (Qui est renvoyé par operator-> Et déréférencé dans operator*) Et un aux*aux est une classe abstraite interne qui contient:

  • un compteur (incrémenté/décrémenté lors de la copie/affectation/destruction)
  • tout ce qui est nécessaire pour rendre l'incrémentation/décrémentation atomique (non nécessaire si une plate-forme atomique spécifique INC/DEC est disponible)
  • un résumé virtual destroy()=0;
  • un destructeur virtuel.

Cette classe aux (le nom réel dépend de l'implémentation) est dérivée par une famille de classes modélisées (paramétrées sur le type donné par le constructeur explicite, disons U dérivé de T), qui ajoutent:

  • un pointeur sur l'objet (identique à T*, mais avec le type réel: cela est nécessaire pour gérer correctement tous les cas où T est une base pour tout ce que U a plusieurs T dans la hiérarchie de dérivation)
  • une copie de l'objet deletor donné en tant que politique de suppression au constructeur explicite (ou le deletor par défaut en train de supprimer p, où p est le U* Ci-dessus)
  • le remplacement de la méthode destroy, appelant le foncteur deleter.

Une esquisse simplifiée peut être celle-ci:

template<class T>
class shared_ptr
{
    struct aux
    {
        unsigned count;

        aux() :count(1) {}
        virtual void destroy()=0;
        virtual ~aux() {} //must be polymorphic
    };

    template<class U, class Deleter>
    struct auximpl: public aux
    {
        U* p;
        Deleter d;

        auximpl(U* pu, Deleter x) :p(pu), d(x) {}
        virtual void destroy() { d(p); } 
    };

    template<class U>
    struct default_deleter
    {
        void operator()(U* p) const { delete p; }
    };

    aux* pa;
    T* pt;

    void inc() { if(pa) interlocked_inc(pa->count); }

    void dec() 
    { 
        if(pa && !interlocked_dec(pa->count)) 
        {  pa->destroy(); delete pa; }
    }

public:

    shared_ptr() :pa(), pt() {}

    template<class U, class Deleter>
    shared_ptr(U* pu, Deleter d) :pa(new auximpl<U,Deleter>(pu,d)), pt(pu) {}

    template<class U>
    explicit shared_ptr(U* pu) :pa(new auximpl<U,default_deleter<U> >(pu,default_deleter<U>())), pt(pu) {}

    shared_ptr(const shared_ptr& s) :pa(s.pa), pt(s.pt) { inc(); }

    template<class U>
    shared_ptr(const shared_ptr<U>& s) :pa(s.pa), pt(s.pt) { inc(); }

    ~shared_ptr() { dec(); }

    shared_ptr& operator=(const shared_ptr& s)
    {
        if(this!=&s)
        {
            dec();
            pa = s.pa; pt=s.pt;
            inc();
        }        
        return *this;
    }

    T* operator->() const { return pt; }
    T& operator*() const { return *pt; }
};

Lorsque l'interopérabilité weak_ptr Est requise, un deuxième compteur (weak_count) Est requis dans aux (sera incrémenté/décrémenté de weak_ptr) Et delete pa ne doit se produire que lorsque les deux compteurs atteignent zéro.

55
Emilio Garavaglia

Comment le comptage des références est-il mis en œuvre?

Une implémentation de pointeur intelligent pourrait être déconstruite, en utilisant conception de classe basée sur des règles1, en:

  • Politique de stockage

  • Politique de propriété

  • Politique de conversion

  • Vérification de la politique

inclus comme paramètres de modèle. Les stratégies de propriété les plus courantes incluent: copie en profondeur, comptage de références, liaison de références et copie destructive.

Le comptage de référence suit le nombre de pointeurs intelligents pointant vers (propriétaire2) le même objet. Lorsque le nombre passe à zéro, l'objet pointee est supprimé3. Le compteur réel pourrait être:

  1. Partagé entre les objets de pointeur intelligent, où chaque pointeur intelligent contient un pointeur sur le compteur de référence:

enter image description here

  1. Inclus uniquement dans une structure supplémentaire qui ajoute un niveau supplémentaire d'indirection à l'objet pointé. Ici, l'espace au-dessus de la tenue d'un compteur dans chaque pointeur intelligent est échangé avec une vitesse d'accès plus lente:

enter image description here

  1. Contenue dans l'objet de pointe lui-même: comptage intrusif des références. L'inconvénient est que l'objet doit être construit a priori avec des facilités de comptage:

    enter image description here

  2. Enfin, la méthode dans votre question, le comptage de références à l'aide de listes doublement liées est appelée liaison de références et elle:

...[1] repose sur l'observation que vous n'avez pas vraiment besoin du nombre réel d'objets pointeurs intelligents pointant vers un objet pointee; il vous suffit de détecter quand ce nombre descend à zéro. Cela conduit à l'idée de conserver une "liste de propriété":

enter image description here

L'avantage de la liaison de référence par rapport au comptage de références est que le premier n'utilise pas de stockage gratuit supplémentaire, ce qui le rend plus fiable: la création d'un pointeur intelligent lié à une référence ne peut pas échouer. L'inconvénient est que la liaison de référence a besoin de plus de mémoire pour sa comptabilité (trois pointeurs contre un seul pointeur plus un entier). De plus, le comptage des références devrait être un peu plus rapide: lorsque vous copiez des pointeurs intelligents, seules une indirection et un incrément sont nécessaires. La gestion des listes est légèrement plus élaborée. En conclusion, vous ne devez utiliser le lien de référence que lorsque la boutique gratuite est rare. Sinon, préférez le comptage des références.

Concernant votre deuxième question:

Est-ce que (std::shared_ptr) Utilise une liste doublement liée?

Tout ce que j'ai pu trouver dans le standard C++ était:

20.7.2.2.6 création shared_ptr
...
7. [Remarque: Ces fonctions allouent généralement plus de mémoire que sizeof(T) pour permettre des structures de comptabilité internes telles que le nombre de références. —Fin note]

Ce qui, à mon avis, exclut les listes doublement liées, car elles ne contiennent pas de nombre réel.

Votre troisième question:

Y a-t-il des pièges à utiliser avec le std::shared_ptr?

La gestion des références, soit le comptage, soit la liaison, est victime de la fuite de ressources connue sous le nom référence cyclique. Ayons un objet A qui contient un pointeur intelligent vers un objet B. De plus, l'objet B contient un pointeur intelligent vers A. Ces deux objets forment une référence cyclique; même si vous n'en utilisez plus, ils s'utilisent. La stratégie de gestion des références ne peut pas détecter de telles références cycliques et les deux objets restent alloués pour toujours.

Étant donné que l'implémentation de shared_ptr Utilise le comptage de références, les références cycliques sont potentiellement un problème. Une chaîne cyclique shared_ptr Peut être rompue en modifiant le code afin que l'une des références soit un weak_ptr. Cela se fait en attribuant des valeurs entre les pointeurs partagés et les pointeurs faibles, mais un pointeur faible n'affecte pas le nombre de références. Si les seuls pointeurs pointant vers un objet sont faibles, l'objet est détruit.


1. Chaque fonctionnalité de conception avec plusieurs implémentations si formulée comme politique.

2. Les pointeurs intelligents, de la même manière que les pointeurs qui pointent vers un objet alloué avec new, pointent non seulement vers cet objet, mais sont également responsables de sa destruction et de la libération de la mémoire qu'il occupe.

3. Sans autres problèmes, si aucun autre pointeur brut n'est utilisé et/ou pointez-le.

[1] Conception C++ moderne: programmation générique et modèles de conception appliqués. Andrei Alexandrescu, 01 février 2001

29
Ziezi

Si vous voulez voir tous les détails sanglants, vous pouvez jeter un œil au boost shared_ptr la mise en oeuvre:

https://github.com/boostorg/smart_ptr

Le comptage des références semble généralement être implémenté avec un compteur et des instructions d'incrémentation/décrémentation atomiques spécifiques à la plate-forme ou un verrouillage explicite avec un mutex (voir le atomic_count_*.hpp fichiers dans espace de noms détaillé ).

4
sth

Y a-t-il des pièges à utiliser avec le std::tr1::shared_ptr?

Oui, si vous créez des cycles dans vos pointeurs de mémoire partagée, la mémoire gérée par le pointeur intelligent ne sera pas recyclée lorsque le dernier pointeur sort de la portée car il y a encore des références au pointeur (c.-à-d., Les cycles provoquent le décompte de références pour ne pas descendre à zéro).

Par exemple:

struct A
{
    std::shared_ptr<A> ptr;
};

std::shared_ptr<A> shrd_ptr_1 = std::make_shared(A());
std::shared_ptr<B> shrd_ptr_2 = std::make_shared(A());
shrd_ptr_1->ptr = shrd_ptr_2;
shrd_ptr_2->ptr = shrd_ptr_1;

Maintenant, même si shrd_ptr_1 et shrd_ptr_2 sortent de la portée, la mémoire qu'ils gèrent n'est pas récupérée car les membres ptr de chacun pointent les uns vers les autres. Bien que ce soit un exemple très naïf d'un tel cycle de mémoire, il peut, si vous utilisez ces types de pointeurs sans aucune discipline, se produire de manière beaucoup plus néfaste et difficile à suivre. Par exemple, je pouvais voir où essayer d'implémenter une liste de liens circulaire où chaque pointeur next est un std::shared_ptr, si vous n'êtes pas trop prudent, cela pourrait entraîner des problèmes.

3
Jason