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
?
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*
Où aux
est une classe abstraite interne qui contient:
virtual destroy()=0;
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:
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)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)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.
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:
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:
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é":
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.
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 quesizeof(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.
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
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é ).
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.