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?
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.
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 vector
s ou d'autres structures de données qui utilisent la sémantique des valeurs, et unique_ptr
s 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_ptr
s, 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é.
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:
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é.
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:
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)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:
C++ isteslf n'a pas de mécanisme natif pour surveiller cet événement (while(are_they_needed)
), donc vous devez approximer avec:
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.
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.
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.
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.
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.
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:
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.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.