Quelle est l'utilité de `enable_shared_from_this`?

349

J'ai parcouru tout enable_shared_from_thisen lisant les exemples Boost.Asio et après avoir lu la documentation, je ne sais toujours pas comment cela devrait être utilisé correctement. Quelqu'un peut-il me donner un exemple et une explication de l'utilisation de ce cours est logique.

fido
la source

Réponses:

362

Il vous permet d'obtenir une shared_ptrinstance valide thislorsque tout ce que vous avez est this. Sans cela, vous n'auriez aucun moyen d'obtenir un accès shared_ptrà thismoins que vous n'en ayez déjà un en tant que membre. Cet exemple de la documentation de boost pour enable_shared_from_this :

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_from_this();
    }
}

int main()
{
    shared_ptr<Y> p(new Y);
    shared_ptr<Y> q = p->f();
    assert(p == q);
    assert(!(p < q || q < p)); // p and q must share ownership
}

La méthode f()renvoie une valeur valide shared_ptr, même si elle n'avait pas d'instance membre. Notez que vous ne pouvez pas simplement faire ceci:

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_ptr<Y>(this);
    }
}

Le pointeur partagé que cela a renvoyé aura un nombre de références différent du "bon", et l'un d'eux finira par perdre et conserver une référence pendant lorsque l'objet est supprimé.

enable_shared_from_thisest devenu une partie de la norme C ++ 11. Vous pouvez également l'obtenir à partir de là ainsi que de boost.

1800 INFORMATION
la source
202
+1. Le point clé est que la technique "évidente" de simplement retourner shared_ptr <Y> (this) est cassée, car cela finit par créer plusieurs objets shared_ptr distincts avec des décomptes de références séparés. Pour cette raison, vous ne devez jamais créer plus d'un shared_ptr à partir du même pointeur brut .
j_random_hacker
3
Il convient de noter qu'en C ++ 11 et versions ultérieures , il est parfaitement valide d'utiliser un std::shared_ptrconstructeur sur un pointeur brut s'il hérite de std::enable_shared_from_this. Je ne sais pas si la sémantique de Boost a été mise à jour pour supporter cela.
Matthew
6
@MatthewHolder Avez-vous un devis pour cela? Sur cppreference.com, je lis "La construction d'un std::shared_ptrpour un objet qui est déjà géré par un autre std::shared_ptrne consultera pas la référence faible stockée en interne et entraînera donc un comportement indéfini." ( en.cppreference.com/w/cpp/memory/enable_shared_from_this )
Thorbjørn Lindeijer
5
Pourquoi tu ne peux pas faire ça shared_ptr<Y> q = p?
Dan M.
2
@ ThorbjørnLindeijer, vous avez raison, c'est C ++ 17 et plus tard. Certaines implémentations ont suivi la sémantique C ++ 16 avant sa sortie. La manipulation appropriée pour C ++ 11 à C ++ 14 doit être utilisée std::make_shared<T>.
Matthew
198

d'après l'article du Dr Dobbs sur les pointeurs faibles, je pense que cet exemple est plus facile à comprendre (source: http://drdobbs.com/cpp/184402026 ):

... un code comme celui-ci ne fonctionnera pas correctement:

int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);

Aucun des deux shared_ptrobjets ne connaît l'autre, donc les deux essaieront de libérer la ressource quand ils seront détruits. Cela entraîne généralement des problèmes.

De même, si une fonction membre a besoin d'un shared_ptrobjet qui possède l'objet auquel elle est appelée, elle ne peut pas simplement créer un objet à la volée:

struct S
{
  shared_ptr<S> dangerous()
  {
     return shared_ptr<S>(this);   // don't do this!
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->dangerous();
   return 0;
}

Ce code a le même problème que l'exemple précédent, bien que sous une forme plus subtile. Lorsqu'il est construit, l' shared_ptobjet r sp1possède la nouvelle ressource allouée. Le code à l'intérieur de la fonction membre S::dangerousne connaît pas cet shared_ptrobjet, donc l' shared_ptrobjet qu'il renvoie est distinct de sp1. Copier le nouvel shared_ptrobjet dans sp2n'aide pas; quand sp2sort de la portée, il libérera la ressource, et quand sp1sortira de la portée, il relâchera la ressource.

Pour éviter ce problème, utilisez le modèle de classe enable_shared_from_this. Le modèle prend un argument de type de modèle, qui est le nom de la classe qui définit la ressource gérée. Cette classe doit, à son tour, être dérivée publiquement du modèle; comme ça:

struct S : enable_shared_from_this<S>
{
  shared_ptr<S> not_dangerous()
  {
    return shared_from_this();
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->not_dangerous();
   return 0;
}

Lorsque vous effectuez cette opération, gardez à l'esprit que l'objet sur lequel vous appelez shared_from_thisdoit appartenir à un shared_ptrobjet. Cela ne fonctionnera pas:

int main()
{
   S *p = new S;
   shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
}
Artashes Aghajanyan
la source
15
Merci, cela illustre le problème mieux résolu que la réponse actuellement acceptée.
goertzenator
2
+1: Bonne réponse. En aparté, au lieu de shared_ptr<S> sp1(new S);cela, il peut être préférable d'utiliser shared_ptr<S> sp1 = make_shared<S>();, voir par exemple stackoverflow.com/questions/18301511/…
Arun
4
Je suis sûr que la dernière ligne doit être lue shared_ptr<S> sp2 = p->not_dangerous();car l'écueil ici est que vous devez créer un shared_ptr de la manière normale avant d'appeler shared_from_this()la première fois! C'est vraiment facile de se tromper! Avant C ++ 17, il est UB d'appeler shared_from_this()avant qu'exactement un shared_ptr ait été créé de façon normale: auto sptr = std::make_shared<S>();ou shared_ptr<S> sptr(new S());. Heureusement, à partir de C ++ 17, cela le fera.
AnorZaken
2
@AnorZaken Bon point. Cela aurait été utile si vous aviez soumis une demande de modification pour apporter ce correctif. Je viens de le faire. L'autre chose utile aurait été que l'affiche ne choisisse pas de noms de méthode subjectifs et contextuels!
underscore_d
30

Voici mon explication, du point de vue des écrous et boulons (la réponse du haut n'a pas «cliqué» avec moi). * Notez que cela est le résultat de la recherche de la source de shared_ptr et enable_shared_from_this fourni avec Visual Studio 2012. Peut-être que d'autres compilateurs implémentent enable_shared_from_this différemment ... *

enable_shared_from_this<T>ajoute une weak_ptr<T>instance privée à Tlaquelle détient le « un vrai nombre de références » pour l'instance de T.

Ainsi, lorsque vous créez pour la première fois un shared_ptr<T>sur un nouveau T *, le faiblesse_ptr interne de ce T * est initialisé avec un décompte de 1. Le nouveau revient shared_ptressentiellement sur ce point weak_ptr.

Tpeut alors, dans ses méthodes, appeler shared_from_thispour obtenir une instance de shared_ptr<T>celle-ci sur le même compte de référence stocké en interne . De cette façon, vous avez toujours un endroit où T*le décompte de ref est stocké plutôt que d'avoir plusieurs shared_ptrinstances qui ne se connaissent pas, et chacun pense shared_ptrque c'est lui qui est responsable du décompte Tet de le supprimer lorsque leur ref -le nombre atteint zéro.

Mackenir
la source
1
C'est correct, et la partie vraiment importante est So, when you first create...parce que c'est une exigence (comme vous le dites, le faiblesse_ptr n'est pas initialisé jusqu'à ce que vous passiez le pointeur des objets dans un ctor shared_ptr!) Et cette exigence est l'endroit où les choses peuvent aller horriblement mal si vous êtes pas prudent. Si vous ne créez aucun shared_ptr avant d'appeler, shared_from_thisvous obtenez UB - de même si vous créez plus d'un shared_ptr, vous obtenez également UB. Vous devez en quelque sorte vous assurer de créer un shared_ptr exactement une fois.
AnorZaken
2
En d'autres termes, l'idée globale de enable_shared_from_thisest fragile au départ, car il s'agit de pouvoir obtenir un shared_ptr<T>de a T*, mais en réalité, lorsque vous obtenez un pointeur, T* til n'est généralement pas sûr de supposer que quelque chose est déjà partagé ou non, et faire la mauvaise supposition est UB.
AnorZaken
" faiblesse_ptr interne est initialisée avec un refcount de 1 " ptr faible à T n'est pas propriétaire ptr intelligent à T. Un ptr faible est une référence intelligente propriétaire à suffisamment d'informations pour faire un ptr propriétaire qui est une "copie" d'autres ptr propriétaires. Un ptr faible n'a pas de compte de référence. Il a accès à un compte de référence, comme tous les propriétaires possédant une référence.
curiousguy
3

Notez que l'utilisation d'un boost :: intrusive_ptr ne souffre pas de ce problème. C'est souvent un moyen plus pratique de contourner ce problème.

blais
la source
Oui, mais enable_shared_from_thisvous permet de travailler avec une API qui accepte spécifiquement shared_ptr<>. À mon avis, une telle API est généralement Doing It Wrong (car il vaut mieux laisser quelque chose de plus élevé dans la pile posséder la mémoire) mais si vous êtes obligé de travailler avec une telle API, c'est une bonne option.
cdunn2001
2
Mieux vaut rester dans la norme autant que possible.
Sergei
3

C'est exactement la même chose en c ++ 11 et versions ultérieures: c'est pour permettre la possibilité de revenir en thistant que pointeur partagé car cela thisvous donne un pointeur brut.

en d'autres termes, cela vous permet de transformer du code comme celui-ci

class Node {
public:
    Node* getParent const() {
        if (m_parent) {
            return m_parent;
        } else {
            return this;
        }
    }

private:

    Node * m_parent = nullptr;
};           

en cela:

class Node : std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getParent const() {
        std::shared_ptr<Node> parent = m_parent.lock();
        if (parent) {
            return parent;
        } else {
            return shared_from_this();
        }
    }

private:

    std::weak_ptr<Node> m_parent;
};           
mchiasson
la source
Cela ne fonctionnera que si ces objets sont toujours gérés par a shared_ptr. Vous voudrez peut-être changer l'interface pour vous assurer que c'est le cas.
curiousguy
1
Vous avez tout à fait raison @curiousguy. Cela va sans dire. J'aime également taper tout mon shared_ptr pour améliorer la lisibilité lors de la définition de mes API publiques. Par exemple, au lieu de std::shared_ptr<Node> getParent const(), je l'exposerais normalement comme à la NodePtr getParent const()place. Si vous avez absolument besoin d'accéder au pointeur brut interne (meilleur exemple: traiter avec une bibliothèque C), il y en a std::shared_ptr<T>::getpour cela, que je déteste mentionner parce que j'ai cet accesseur de pointeur brut utilisé trop de fois pour la mauvaise raison.
mchiasson
-3

Une autre façon consiste à ajouter un weak_ptr<Y> m_stubmembre dans le class Y. Puis écrire:

shared_ptr<Y> Y::f()
{
    return m_stub.lock();
}

Utile lorsque vous ne pouvez pas changer la classe dont vous dérivez (par exemple, étendre la bibliothèque d'autres personnes). N'oubliez pas d'initialiser le membre, par exemple par m_stub = shared_ptr<Y>(this), il est valide même pendant un constructeur.

C'est OK s'il y a plus de stubs comme celui-ci dans la hiérarchie d'héritage, cela n'empêchera pas la destruction de l'objet.

Edit: Comme l'a correctement souligné l'utilisateur nobar, le code détruirait l'objet Y lorsque l'affectation est terminée et les variables temporaires sont détruites. Par conséquent, ma réponse est incorrecte.

PetrH
la source
4
Si votre intention ici est de produire un shared_ptr<>qui ne supprime pas sa pointe, c'est exagéré. Vous pouvez simplement dire return shared_ptr<Y>(this, no_op_deleter);no_op_deleterun objet de fonction unaire prend Y*et ne fait rien.
John Zwinck
2
Il semble peu probable que ce soit une solution de travail. m_stub = shared_ptr<Y>(this)va construire et détruire immédiatement un shared_ptr temporaire à partir de cela. Lorsque cette instruction est terminée, thissera supprimée et toutes les références suivantes seront pendantes.
nobar
2
L'auteur reconnaît que cette réponse est erronée, il pourrait donc probablement la supprimer. Mais il s'est connecté pour la dernière fois il y a 4,5 ans, donc il est peu probable qu'il le fasse - quelqu'un avec des pouvoirs supérieurs pourrait-il supprimer ce hareng rouge?
Tom Goodfellow
si vous regardez l'implémentation de enable_shared_from_this, il conserve un weak_ptrde lui-même (rempli par le ctor), retourné comme un shared_ptrlorsque vous appelez shared_from_this. En d'autres termes, vous dupliquez ce enable_shared_from_thisqui fournit déjà.
mchiasson du