web-dev-qa-db-fra.com

std :: shared_ptr en dernier recours?

Je regardais juste les flux "Going Native 2012" et j'ai remarqué la discussion sur std::shared_ptr. J'ai été un peu surpris d'entendre le point de vue quelque peu négatif de Bjarne sur std::shared_ptr et son commentaire selon lequel il devrait être utilisé en "dernier recours" lorsque la durée de vie d'un objet est incertaine (ce qui, selon moi, devrait rarement être le cas).

Quelqu'un voudrait-il expliquer cela un peu plus en profondeur? Comment programmer sans std::shared_ptr tout en gérant les durées de vie des objets de manière sûre?

62
ronag

Si vous pouvez éviter la propriété partagée, votre application sera plus simple et plus facile à comprendre et donc moins vulnérable aux bogues introduits lors de la maintenance. Les modèles de propriété complexes ou peu clairs ont tendance à conduire à des couplages difficiles à suivre de différentes parties de l'application à travers un état partagé qui peuvent ne pas être facilement traçables.

Compte tenu de cela, il est préférable d'utiliser des objets avec une durée de stockage automatique et d'avoir des sous-objets "value". A défaut, unique_ptr peut être une bonne alternative avec shared_ptr étant - sinon un dernier recours - quelque part dans la liste des outils souhaitables.

58
CB Bailey

Le monde dans lequel Bjarne vit est très ... académique, faute d'un meilleur terme. Si votre code peut être conçu et structuré de manière à ce que les objets aient des hiérarchies relationnelles très délibérées, de sorte que les relations de propriété soient rigides et inflexibles, le code circule dans une seule direction (de haut niveau à bas niveau) et les objets ne parlent qu'aux personnes les plus basses de la hiérarchie, vous n'aurez pas besoin de beaucoup de shared_ptr. C'est quelque chose que vous utilisez dans ces rares occasions où quelqu'un doit enfreindre les règles. Mais sinon, vous pouvez simplement tout coller dans vectors ou d'autres structures de données qui utilisent la sémantique des valeurs, et unique_ptrs pour les choses que vous devez allouer séparément.

Bien que ce soit un monde formidable où vivre, ce n'est pas ce que vous avez à faire tout le temps. Si vous ne pouvez pas organiser votre code de cette façon, car la conception du système que vous essayez de créer signifie qu'il est impossible (ou tout simplement profondément désagréable), alors vous allez vous retrouver la propriété partagée d'objets de plus en plus.

Dans un tel système, tenir des pointeurs nus n'est ... pas dangereux exactement, mais cela soulève des questions. La grande chose à propos de shared_ptr est qu'il fournit raisonnable des garanties syntaxiques sur la durée de vie de l'objet. Peut-il être cassé? Bien sûr. Mais les gens peuvent aussi const_cast des choses; soins de base et alimentation de shared_ptr devrait offrir une qualité de vie raisonnable aux objets attribués dont la propriété doit être partagée.

Ensuite, il y a weak_ptrs, qui ne peut pas être utilisé en l'absence d'un shared_ptr. Si votre système est structuré de manière rigide, vous pouvez stocker un pointeur nu sur un objet, sachant que la structure de l'application garantit que l'objet pointé vous survivra. Vous pouvez appeler une fonction qui renvoie un pointeur sur une valeur interne ou externe (trouver un objet nommé X, par exemple). Dans un code correctement structuré, cette fonction ne serait disponible que si la durée de vie de l'objet était garantie supérieure à la vôtre; ainsi, stocker ce pointeur nu dans votre objet est très bien.

Étant donné que cette rigidité n'est pas toujours possible à atteindre dans les systèmes réels, vous avez besoin d'un moyen pour assurer raisonnablement la durée de vie. Parfois, vous n'avez pas besoin d'une pleine propriété; parfois, il suffit de savoir quand le pointeur est mauvais ou bon. C'est là que weak_ptr entre en jeu. Dans certains cas, j'ai pourrais avoir utilisé un unique_ptr ou boost::scoped_ptr, mais j'ai dû utiliser un shared_ptr parce que j'ai spécifiquement nécessaire pour donner à quelqu'un un pointeur "volatil". Un pointeur dont la durée de vie était indéterminée, et ils pouvaient demander quand ce pointeur a été détruit.

Un moyen sûr de survivre lorsque l'état du monde est indéterminé.

Cela aurait-il pu être fait par un appel de fonction pour obtenir le pointeur, au lieu de via weak_ptr? Oui, mais cela pourrait plus facilement être brisé. Une fonction qui renvoie un pointeur nu n'a aucun moyen de suggérer syntaxiquement que l'utilisateur ne fait pas quelque chose comme stocker ce pointeur à long terme. Renvoyer un shared_ptr rend également trop facile pour quelqu'un de simplement le stocker et potentiellement prolonger la durée de vie d'un objet. Renvoyer un weak_ptr suggère cependant fortement que le stockage du shared_ptr vous obtenez de lock est une ... idée douteuse. Cela ne vous empêchera pas de le faire, mais rien en C++ ne vous empêche de casser le code. weak_ptr offre une résistance minimale à l'action naturelle.

Maintenant, cela ne veut pas dire que shared_ptr ne peut pas être surutilisé; c'est certainement possible. Surtout pré -unique_ptr, il y a eu de nombreux cas où je viens d'utiliser un boost::shared_ptr parce que je devais passer un pointeur RAII ou le mettre dans une liste. Sans déplacer la sémantique et unique_ptr, boost::shared_ptr était la seule vraie solution.

Et vous pouvez l'utiliser dans des endroits où cela n'est pas nécessaire. Comme indiqué ci-dessus, une structure de code appropriée peut éliminer le besoin de certaines utilisations de shared_ptr. Mais si votre système ne peut pas être structuré comme tel et continue à faire ce qu'il faut, shared_ptr sera d'une grande utilité.

48
Nicol Bolas

Je ne crois pas avoir déjà utilisé std::shared_ptr.

La plupart du temps, un objet est associé à une collection à laquelle il appartient pendant toute sa durée de vie. Dans ce cas, vous pouvez simplement utiliser whatever_collection<o_type> ou whatever_collection<std::unique_ptr<o_type>>, cette collection étant membre d'un objet ou d'une variable automatique. Bien sûr, si vous n'avez pas besoin d'un nombre dynamique d'objets, vous pouvez simplement utiliser un tableau automatique de taille fixe.

Aucune itération à travers la collection ou toute autre opération sur l'objet ne nécessite une fonction d'assistance pour partager la propriété ... il tilise l'objet, puis retourne, et l'appelant garantit que l'objet reste en vie pendant tout l'appel . Il s'agit de loin du contrat le plus utilisé entre l'appelant et l'appelé.


Nicol Bolas a commenté que "si un objet tient sur un pointeur nu et que cet objet meurt ... oups." et "Les objets doivent garantir que l'objet vit tout au long de la vie de cet objet. Seulement shared_ptr peut faire ça."

Je n'achète pas cet argument. Du moins pas que shared_ptr résout ce problème. Qu'en est-il de:

  • Si une table de hachage conserve un objet et que le code de hachage de cet objet change ... oups.
  • Si une fonction itère un vecteur et qu'un élément est inséré dans ce vecteur ... oups.

Comme la récupération de place, l'utilisation par défaut de shared_ptr encourage le programmeur à ne pas penser au contrat entre les objets, ou entre la fonction et l'appelant. Penser aux conditions préalables et postconditions correctes est nécessaire, et la durée de vie de l'objet n'est qu'un petit morceau de cette tarte plus grande.

Les objets ne "meurent" pas, un morceau de code les détruit. Et jeter shared_ptr au problème au lieu de comprendre le contrat d'appel est une fausse sécurité.

39
Ben Voigt

Je préfère ne pas penser en termes absolus (comme "dernier recours") mais par rapport au domaine problématique.

C++ peut offrir un certain nombre de façons différentes de gérer la durée de vie. Certains d'entre eux tentent de reconduire les objets de manière pilotée par la pile. D'autres tentent d'échapper à cette limitation. Certains d'entre eux sont "littéraux", d'autres sont des approximations.

En fait, vous pouvez:

  1. tilisez une sémantique de valeur pure. Fonctionne pour des objets relativement petits où ce qui est important sont des "valeurs" et non des "identités", où vous pouvez supposer que deux Person ayant le même name sont identiques personne (mieux: deux représentations d'une même personne ). La durée de vie est accordée par la pile de la machine, la fin -essentiellement- n'a pas d'importance pour le programme (car une personne est-elle s nom , peu importe ce que Person le porte)
  2. tiliser les objets alloués par pile, et les références ou pointeurs associés: autorise le polymorphisme et accorde la durée de vie des objets. Pas besoin de "pointeurs intelligents", car vous vous assurez qu'aucun objet ne peut être "pointé" par des structures qui laissent dans la pile plus longtemps que l'objet vers lequel elles pointent (créez d'abord l'objet, puis les structures qui s'y réfèrent).
  3. tiliser les objets alloués par tas gérés par pile: c'est ce que font std :: vector et tous les conteneurs, et wat std::unique_ptr le fait (vous pouvez le considérer comme un vecteur de taille 1). Encore une fois, vous admettez que l'objet commence à exister (et finit son existence) avant (après) la structure de données à laquelle ils se réfèrent.

La faiblesse de ces mehtods est que les types et quantités d'objets ne peuvent pas varier lors de l'exécution d'appels de niveau de pile plus profond en fonction de leur emplacement. Toutes ces techniques "échouent" leur force dans toutes les situations où la création et la suppression d'objet sont la conséquence des activités de l'utilisateur, de sorte que le type d'exécution de l'objet n'est pas connu au moment de la compilation et il peut y avoir des sur-structures faisant référence aux objets l'utilisateur demande de supprimer d'un appel de fonction au niveau de la pile plus profond. Dans ce cas, vous devez soit:

  • introduire une certaine discipline dans la gestion des objets et des structures de référence associées ou ...
  • aller en quelque sorte vers le côté obscur de "échapper à la durée de vie basée sur la pile pure": l'objet doit partir indépendamment des fonctions qui les ont créés. Et doit partir ... jusqu'à ce qu'ils soient nécessaires .

C++ isteslf n'a pas de mécanisme natif pour surveiller cet événement (while(are_they_needed)), donc vous devez approximer avec:

  1. tiliser la propriété partagée: la vie des objets est liée à un "compteur de référence": fonctionne si la "propriété" peut être organisée hiérarchiquement, échoue là où des boucles de propriété peuvent exister. C'est ce que fait std :: shared_ptr. Et faiblesse_ptr peut être utilisée pour rompre la boucle. Cela fonctionne la plupart du temps mais échoue dans le grand design, où de nombreux concepteurs travaillent dans différentes équipes et il n'y a pas de raison claire (quelque chose provenant d'une certaine exigence) de savoir qui doit renfermer quoi (l'exemple typique est une double chaîne aimée: le précédent dû le suivant référant le précédent ou le suivant possédant le précédent référant au suivant? En cas de besoin, les solutions sont équivalentes, et dans un grand projet, vous risquez de les mélanger)
  2. tilisez un tas de collecte des ordures: Vous ne vous souciez simplement pas de la durée de vie. Vous exécutez le collecteur de temps en temps et ce qui est inaccessible est considéré comme "plus nécessaire" et ... eh bien ... ahem ... détruit? finalisé? congelé?. Il existe un certain nombre de collecteurs GC, mais je n'en trouve jamais qui soit vraiment compatible C++. La plupart d'entre eux libèrent de la mémoire, sans se soucier de la destruction d'objets.
  3. tilisez un ramasse-miettes compatible C++, avec une interface de méthodes standard appropriée. Bonne chance pour le trouver.

En passant de la toute première solution à la dernière, la quantité de structure de données auxiliaire requise pour gérer la durée de vie des objets augmente, tout comme le temps passé à l'organiser et à le maintenir.

Le garbage collector a un coût, shared_ptr en a moins, unique_ptr encore moins et les objets gérés en pile en ont très peu.

shared_ptr Est-il le "dernier recours"?. Non, ce n'est pas le cas: les récupérateurs sont les derniers recours. shared_ptr Est en fait le std:: Dernier recours proposé. Mais peut-être la bonne solution, si vous êtes dans la situation que j'ai expliquée.

16
Emilio Garavaglia

La seule chose mentionnée par Herb Sutter dans une session ultérieure est que chaque fois que vous copiez un shared_ptr<> il doit y avoir un incrément/décrément interverrouillé. Sur le code multi-thread sur un système multi-core, la synchronisation de la mémoire n'est pas négligeable. Étant donné le choix, il est préférable d'utiliser soit une valeur de pile, soit un unique_ptr<> et passez des références ou des pointeurs bruts.

9
Eclipse

Je ne me souviens pas si le dernier "recours" était le mot exact qu'il a utilisé, mais je crois que le sens réel de ce qu'il a dit était le dernier "choix": étant donné des conditions de propriété claires; unique_ptr, faiblesse_ptr, shared_ptr et même des pointeurs nus ont leur place.

Une chose sur laquelle ils se sont tous mis d'accord, c'est que nous (développeurs, auteurs de livres, etc.) sommes tous dans la "phase d'apprentissage" de C++ 11 et que les modèles et les styles sont en cours de définition.

À titre d'exemple, Herb a expliqué que nous devrions nous attendre à de nouvelles éditions de certains des livres séminaux C++, tels que Effective C++ (Meyers) et C++ Coding Standards (Sutter & Alexandrescu), quelques années, tandis que l'expérience et les meilleures pratiques de l'industrie avec C++ 11 disparaissent.

7
Eddie Velasquez

Je pense que ce qu'il veut dire, c'est qu'il devient courant pour tout le monde d'écrire shared_ptr chaque fois qu'ils ont pu écrire un pointeur standard (comme une sorte de remplacement global), et qu'il est utilisé comme une copie au lieu de réellement concevoir ou au moins planification de la création et de la suppression d'objets.

L'autre chose que les gens oublient (en plus du goulot d'étranglement de verrouillage/mise à jour/déverrouillage mentionné dans le document ci-dessus), c'est que shared_ptr seul ne résout pas les problèmes de cycle. Vous pouvez toujours divulguer des ressources avec shared_ptr:

L'objet A, contient un pointeur partagé vers un autre objet A, l'objet B crée A a1 et A a2 et affecte a1.otherA = a2; et a2.otherA = a1; Maintenant, les pointeurs partagés de l'objet B qu'il a utilisés pour créer a1, a2 sortent de la portée (disons à la fin d'une fonction). Maintenant, vous avez une fuite - personne d'autre ne fait référence à a1 et a2, mais ils se réfèrent l'un à l'autre, donc leur nombre de références est toujours 1, et vous avez fui.

C'est l'exemple simple, lorsque cela se produit dans du code réel, cela se produit généralement de manière compliquée. Il y a une solution avec faiblesse_ptr, mais tellement de gens font maintenant juste shared_ptr partout et ne connaissent même pas le problème de fuite ou même de faiblesse_ptr.

Pour résumer: je pense que les commentaires référencés par l'OP se résument à ceci:

Peu importe la langue dans laquelle vous travaillez (géré, non géré ou quelque chose entre les deux avec des nombres de référence comme shared_ptr), vous devez comprendre et décider intentionnellement de la création, de la durée de vie et de la destruction d'objets.

edit: même si cela signifie "inconnu, j'ai besoin d'utiliser un shared_ptr", vous y avez toujours pensé et le faites intentionnellement.

5
anon

Je répondrai de mon expérience avec Objective-C, un langage où tous les objets sont comptés par référence et alloués sur le tas. En raison d'une façon unique de traiter les objets, les choses sont beaucoup plus faciles pour le programmeur. Cela a permis de définir des règles standard qui, lorsqu'elles sont respectées, garantissent la robustesse du code et aucune fuite de mémoire. Il a également permis à des optimisations de compilateur intelligentes d'émerger comme le récent ARC (comptage automatique des références).

Mon point est que shared_ptr devrait être votre première option plutôt que le dernier recours. Utilisez le comptage des références par défaut et d'autres options uniquement si vous êtes sûr de ce que vous faites. Vous serez plus productif et votre code sera plus robuste.

3
Dimitris

Je vais essayer de répondre à la question:

Comment pouvons-nous programmer sans std :: shared_ptr tout en gérant les durées de vie des objets de manière sûre?

C++ a un grand nombre de façons différentes de faire de la mémoire, par exemple:

  1. Utilisez struct A { MyStruct s1,s2; }; Au lieu de shared_ptr dans la portée de la classe. C'est uniquement pour les programmeurs avancés car cela nécessite que vous compreniez comment fonctionnent les dépendances et nécessite la capacité de contrôler suffisamment les dépendances pour les restreindre à une arborescence. L'ordre des classes dans le fichier d'en-tête en est un aspect important. Il semble que cette utilisation soit déjà courante avec les types C++ natifs intégrés, mais son utilisation avec les classes définies par le programmeur semble être moins utilisée en raison de ces problèmes de dépendance et d'ordre des classes. Cette solution a également des problèmes avec sizeof. Les programmeurs voient les problèmes dans ceci comme une exigence pour utiliser des déclarations directes ou des #includes inutiles et donc de nombreux programmeurs se rabattront sur une solution inférieure de pointeurs et plus tard sur shared_ptr.
  2. Utilisez MyClass &find_obj(int i); + clone () au lieu de shared_ptr<MyClass> create_obj(int i);. De nombreux programmeurs veulent créer des usines pour créer de nouveaux objets. shared_ptr est idéalement adapté à ce type d'utilisation. Le problème est qu'il suppose déjà une solution de gestion de mémoire complexe utilisant l'allocation de tas/magasin libre, au lieu d'une solution plus simple basée sur la pile ou les objets. Une bonne hiérarchie de classes C++ prend en charge tous les schémas de gestion de la mémoire, pas seulement l'un d'entre eux. La solution basée sur les références peut fonctionner si l'objet renvoyé est stocké à l'intérieur de l'objet contenant, au lieu d'utiliser la variable de portée de fonction locale. Le transfert de propriété de l'usine au code utilisateur doit être évité. Copier l'objet après avoir utilisé find_obj () est un bon moyen de le gérer - les constructeurs de copie normaux et le constructeur normal (de classe différente) avec le paramètre de référence ou clone () pour les objets polymorphes peuvent le gérer.
  3. Utilisation de références au lieu de pointeurs ou shared_ptrs. Chaque classe c ++ possède des constructeurs et chaque membre de données de référence doit être initialisé. Cette utilisation peut éviter de nombreuses utilisations des pointeurs et shared_ptrs. Vous avez juste besoin de choisir si votre mémoire est à l'intérieur ou à l'extérieur de l'objet, et choisissez la solution de structure ou la solution de référence en fonction de la décision. Les problèmes avec cette solution sont généralement liés à l'évitement des paramètres du constructeur, ce qui est une pratique courante mais problématique et à une mauvaise compréhension de la façon dont les interfaces pour les classes doivent être conçues.
1
tp1