Je regardais simplement les flux "Going Native 2012" et j'ai remarqué la discussion à ce sujet std::shared_ptr
. J'ai été un peu surpris d'entendre le point de vue un peu négatif de Bjarne std::shared_ptr
et son commentaire qu'il devrait être utilisé en "dernier recours" lorsque la durée de vie d'un objet est incertaine (ce qui, selon lui, devrait rarement être le cas).
Quelqu'un voudrait-il expliquer cela un peu plus en profondeur? Comment pouvons-nous programmer sans std::shared_ptr
gérer et toujours gérer la durée de vie des objets de manière sûre ?
c++
c++11
smart-pointer
Ronag
la source
la source
Réponses:
Si vous pouvez éviter la propriété partagée, votre application sera plus simple et plus compréhensible, et donc moins susceptible aux bugs introduits lors de la maintenance. Les modèles de propriété complexes ou peu clairs conduisent généralement à des couplages difficiles à suivre entre différentes parties de l'application via un état partagé qui peut ne pas être facilement traçable.
Compte tenu de cela, il est préférable d’utiliser des objets à durée de stockage automatique et d’avoir des sous-objets "valeur". À défaut, cela
unique_ptr
peut constituer une bonne alternativeshared_ptr
, sinon ultime, à la liste des outils souhaitables.la source
Le monde dans lequel vit Bjarne 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 un sens (du plus bas au plus bas), et les objets ne parlent qu'aux plus bas. la hiérarchie, alors vous ne trouverez pas beaucoup besoin
shared_ptr
. C'est quelque chose que vous utilisez dans les rares occasions où quelqu'un doit enfreindre les règles. Mais sinon, vous pouvez simplement coller tout ce qui est dansvector
s ou dans d'autres structures de données qui utilisent la sémantique de valeur, etunique_ptr
s pour les choses que vous devez allouer individuellement.C'est un monde où il fait bon vivre, mais ce n'est pas ce que vous devez faire tout le temps. Si vous ne pouvez pas organiser votre code de cette manière, car la conception du système que vous essayez de créer signifie qu'il est impossible (ou tout simplement profondément déplaisant), vous allez alors vous retrouver de plus en plus confronté à la propriété partagée des objets. .
Dans un tel système, tenir des pointeurs nus n’est pas… dangereux, mais cela soulève des questions. L'avantage
shared_ptr
est qu'il fournit des garanties syntaxiques raisonnables sur la durée de vie de l'objet. Peut-il être cassé? Bien sûr. Mais les gens peuvent aussi desconst_cast
choses; les soins de base et l'alimentation des enfantsshared_ptr
devraient fournir 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 peuvent pas être utilisés en l'absence deshared_ptr
. Si votre système est structuré de manière rigide, vous pouvez stocker un pointeur nu sur un objet, en 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 (recherchez 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 supérieure à la votre; ainsi, stocker ce pointeur nu dans votre objet est très bien.Étant donné que cette rigidité n’est pas toujours possible dans les systèmes réels, vous avez besoin d’un moyen raisonnable d’en assurer la durée de vie. Parfois, vous n'avez pas besoin de la pleine propriété. Parfois, il suffit de savoir quand le pointeur est mauvais ou bon. C’est là que l’
weak_ptr
intervient. Dans certains cas, j’aurais pu utiliser ununique_ptr
ouboost::scoped_ptr
, mais je devais utiliser unshared_ptr
parce que j’avais précisément besoin de 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 avait é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 être plus facilement cassé. Une fonction qui retourne un pointeur nu n'a aucun moyen de suggérer syntaxiquement que l'utilisateur ne fasse pas quelque chose comme stocker ce pointeur à long terme. Le renvoi d'unshared_ptr
rend également beaucoup trop facile pour quelqu'un de simplement le stocker et potentiellement prolonger la durée de vie d'un objet. Restituer unweak_ptr
message suggère toutefois fortement que le stockage de ce queshared_ptr
vous obtenezlock
est une idée ... douteuse. Cela ne vous empêchera pas de le faire, mais rien en C ++ ne vous empêchera de casser du code.weak_ptr
fournit une résistance minimale de faire la chose naturelle.Maintenant, cela
shared_ptr
ne veut pas dire que vous ne pouvez pas en abuser ; cela peut certainement. Surtout avantunique_ptr
, il y avait beaucoup de cas où j'ai simplement utilisé unboost::shared_ptr
parce que je devais passer un pointeur RAII ou le mettre dans une liste. Sans la sémantique de mouvement etunique_ptr
,boost::shared_ptr
était la seule vraie solution.Et vous pouvez l'utiliser dans des endroits où cela est vraiment inutile. 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é en tant que tel et continue à faire ce qu’il faut,shared_ptr
il sera d’une grande utilité.la source
shared_ptr
est idéal pour les systèmes où c ++ est intégré à un langage de script tel que python. L'utilisation duboost::python
comptage de références côté c ++ et python coopère grandement; n'importe quel objet de c ++ peut toujours être tenu en python et il ne mourra pas.shared_ptr
. Les deux utilisent leurs propres implémentations deintrusive_ptr
. J'en parle uniquement parce que ce sont deux exemples concrets d'applications volumineuses écrites en C ++shared_ptr
s'appliquer égalementintrusive_ptr
: il s'oppose à l'ensemble du concept de propriété partagée et non à une orthographe spécifique du concept. Donc , à des fins de cette question, ce sont deux exemples concrets de grandes applications qui font usageshared_ptr
. (Et, en plus, ils démontrent queshared_ptr
c'est utile même quand ça ne permet pasweak_ptr
.)Je ne crois pas en 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>
ouwhatever_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.Ni l'itération dans la collection, ni aucune autre opération sur l'objet ne nécessite une fonction d'assistance pour partager la propriété ... elle utilise l'objet, puis revient et l'appelant garantit que l'objet reste en vie pendant tout l'appel . C’est de loin le contrat le plus utilisé entre appelant et appelé.
Nicol Bolas a commenté que "Si un objet tient sur un pointeur nu et que cet objet meurt ... oups." et "Les objets doivent s'assurer que l'objet vit à travers la vie de cet objet. Seul
shared_ptr
peut le faire."Je n'achète pas cet argument. Du moins, cela ne
shared_ptr
résout pas ce problème. Qu'en est-il de:Tout comme la récupération de place, l'utilisation par défaut de
shared_ptr
encourage le programmeur à ne pas penser au contrat entre objets, ni entre fonction et appelant. Il est nécessaire de penser aux conditions préalables et aux conditions postérieures correctes, et la durée de vie d’un objet n’est qu’un tout petit morceau de ce gâteau plus grand.Les objets ne "meurent" pas, un morceau de code les détruit. Et jeter
shared_ptr
le problème au lieu de déterminer le contrat d’appel est une fausse sécurité.la source
shared_ptr
etweak_ptr
ont été conçus pour éviter. Bjarne essaie de vivre dans un monde où tout a une belle durée de vie, explicite, et tout est construit autour de cela. Et si vous pouvez construire ce monde, tant mieux. Mais ce n'est pas comme ça dans le monde réel. Les objets doivent s'assurer que l'objet vit à travers la vie de cet objet. Seulementshared_ptr
peut faire ça.shared_ptr
n'atténue qu'une modification extérieure spécifique, et même pas la plus courante. Et ce n'est pas la responsabilité de l'objet de s'assurer que sa durée de vie est correcte, si le contrat d'appel de fonction spécifie le contraire.unique_ptr
, exprimant qu'un seul pointeur sur l'objet existe et qu'il en est le propriétaire.shared_ptr
, il doit quand même renvoyer ununique_ptr
. La conversion deunique_ptr
versshared_ptr
est facile, mais l'inverse est logiquement impossible.Je préfère ne pas penser en termes absolus (comme "dernier recours") mais par rapport au domaine du problème.
Le C ++ peut offrir différentes manières de gérer la durée de vie. Certains d’entre eux essaient de re-conduire les objets de manière empilée. Certains autres essaient d'échapper à cette limitation. Certains d'entre eux sont "littéraux", d'autres sont des approximations.
En fait, vous pouvez:
Person
ayant la même chosename
sont la même personne (mieux: deux représentations d'une même personne ). La durée de vie est octroyée par la pile de machines, la fin est essentielle pour le programme (puisqu’une personne est son nom , peu importe cePerson
qu’elle transporte)std::unique_ptr
(vous pouvez le penser comme un vecteur de taille 1). De nouveau, vous admettez que l’objet commence à exister (et finit par exister) avant (après) la structure de données à laquelle il se réfère.La faiblesse de cette méthode réside dans le fait que les types et quantités d'objets ne peuvent pas varier lors de l'exécution d'appels plus profonds au niveau de la pile en fonction de l'endroit où ils sont créés. Toutes ces techniques "échouent" dans toutes les situations où la création et la suppression d'un objet sont la conséquence d'activités de l'utilisateur, de sorte que le type d'exécution de l'objet ne soit pas connu au moment de la compilation et qu'il puisse exister des sur-structures faisant référence à des objets. L'utilisateur demande à supprimer d'un appel de fonction plus profond au niveau de la pile. Dans ce cas, vous devez soit:
C ++ isteslf ne dispose d'aucun mécanisme natif pour surveiller cet événement (
while(are_they_needed)
), vous devez donc effectuer une approximation avec:Pour passer 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, au fur et à mesure que le temps passe à l’organiser et à la maintenir.
Le garbage collector a coûté, shared_ptr en a moins, unique_ptr encore moins, et les objets gérés en pile en ont très peu.
Est-ce
shared_ptr
le "dernier recours" ?. Non, ce n'est pas le cas: le dernier recours est celui des éboueurs.shared_ptr
est en fait lestd::
dernier recours proposé. Mais peut être la bonne solution, si vous êtes dans la situation que j'ai expliquée.la source
Herb Sutter a mentionné dans une session ultérieure que chaque fois que vous copiez
shared_ptr<>
, un incrément / décrément imbriqué doit se produire. Sur un code multithread sur un système multicœur, la synchronisation de la mémoire n’est pas anodine. Étant donné le choix, il est préférable d’utiliser soit une valeur de pile, soit ununique_ptr<>
et de passer des références ou des pointeurs bruts.la source
shared_ptr
par référence lvalue ou rvalue ...shared_ptr
solution miracle comme solution miracle, car elle résoudra tous vos problèmes de fuite de mémoire simplement parce qu'elle est conforme à la norme. C'est un piège tentant, mais il est toujours important d'être conscient de la propriété des ressources, et à moins que cette propriété ne soit partagée,shared_ptr<>
l'option n'est pas la meilleure.Je ne me souviens pas si le terme "recours" était exactement le mot qu'il a utilisé, mais je crois que le sens réel de ce qu'il a dit était le dernier "choix": conditions de propriété claires; unique_ptr, faible_ptr, shared_ptr et même les pointeurs nus ont leur place.
Une chose sur laquelle ils ont tous convenu est que nous sommes (développeurs, auteurs de livres, etc.) tous dans la "phase d'apprentissage" de C ++ 11 et que les modèles et les styles sont en cours de définition.
Herb a expliqué, à titre d'exemple, que nous devrions nous attendre à de nouvelles éditions de certains ouvrages phares du C ++, tels que Effective C ++ (Meyers) et C ++ Coding Standards (Sutter & Alexandrescu), dans deux ans, alors que l'industrie et les meilleures pratiques de C ++ 11 casseroles.
la source
Je pense qu'il en vient à comprendre qu'il est de plus en plus 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 échappatoire au lieu d'être réellement conçu ou du moins planification de la création et de la suppression d'objets.
L'autre chose que les gens oublient (à part le goulot d'étranglement de verrouillage / mise à jour / déverrouillage mentionné dans le contenu ci-dessus), c'est que shared_ptr ne résout pas à lui seul les problèmes de cycle. Vous pouvez toujours perdre des ressources avec shared_ptr:
L'objet A, contient un pointeur partagé sur un autre objet. L'objet B crée A a1 et A a2 et attribue a1.otherA = a2; et a2.otherA = a1; Maintenant, les pointeurs partagés de l'objet B 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 de sorte que leur nombre de références est toujours égal à 1 et que vous avez une fuite.
C'est un 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 faible_ptr, mais tant de gens font maintenant juste du shared_ptr partout et ne connaissent même pas le problème de fuite ou même de faible_ptr.
Pour conclure, je pense que les commentaires référencés par le PO se résument à ceci:
Quelle que soit la langue dans laquelle vous travaillez (gérée, non gérée ou quelque chose entre les chiffres de référence tels que 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, je dois utiliser un shared_ptr", vous y avez encore pensé et le faites intentionnellement.
la source
Je réponds de mon expérience avec Objective-C, un langage dans lequel tous les objets sont comptés et alloués sur le tas. Parce qu’il n’ya qu’une façon de traiter les objets, les choses sont beaucoup plus simples pour le programmeur. Cela a permis de définir des règles standard qui, une fois appliquées, garantissent la robustesse du code et l’absence de fuite de mémoire. Cela a également permis à des optimisations intelligentes du compilateur 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 de références par défaut et d'autres options uniquement si vous êtes certain de ce que vous faites. Vous serez plus productif et votre code sera plus robuste.
la source
Je vais essayer de répondre à la question:
C ++ a un grand nombre de façons différentes de faire de la mémoire, par exemple:
struct A { MyStruct s1,s2; };
plutôt shared_ptr dans la portée de la classe. Cela concerne uniquement les programmeurs avancés, car cela nécessite de comprendre le fonctionnement des dépendances et de maîtriser suffisamment les dépendances pour les restreindre à une arborescence. L'ordre des classes dans le fichier d'en-tête est un aspect important de cela. Il semble que cette utilisation soit déjà commune avec les types natifs c ++ 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 aussi des problèmes avec sizeof. Les programmeurs voient dans les problèmes que cela pose comme une obligation d'utiliser des déclarations anticipées ou des #inclus non nécessaires. Ainsi, de nombreux programmeurs retomberont sur une solution inférieure de pointeurs, puis sur shared_ptr.MyClass &find_obj(int i);
+ clone () au lieu deshared_ptr<MyClass> create_obj(int i);
. Beaucoup de programmeurs veulent créer des usines pour créer de nouveaux objets. shared_ptr est parfaitement adapté à ce type d'utilisation. Le problème est qu’elle suppose déjà une solution de gestion de la mémoire complexe utilisant l’allocation de tas / librairie, au lieu d’une solution plus simple basée sur la pile ou sur les objets. Une bonne hiérarchie de classes C ++ prend en charge tous les schémas de gestion de la mémoire, pas un seul. 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 la 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 refrerence ou clone () pour les objets polymorphes peuvent le gérer.la source
unique_ptr
convient le mieux aux usines. Vous pouvez transformer ununique_ptr
en unshared_ptr
, mais il est logiquement impossible d'aller dans l'autre sens.