Si j'ai une fonction qui doit fonctionner avec a shared_ptr
, ne serait-il pas plus efficace de lui passer une référence (pour éviter de copier l' shared_ptr
objet)? Quels sont les effets secondaires possibles? J'envisage deux cas possibles:
1) à l'intérieur de la fonction, une copie de l'argument est faite, comme dans
ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)
{
...
m_sp_member=sp; //This will copy the object, incrementing refcount
...
}
2) à l'intérieur de la fonction, l'argument est uniquement utilisé, comme dans
Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here
{
...
sp->do_something();
...
}
Je ne vois pas dans les deux cas une bonne raison de passer la boost::shared_ptr<foo>
valeur par valeur plutôt que par référence. Le passage par valeur n'incrémenterait que "temporairement" le nombre de références en raison de la copie, puis le décrémenterait lors de la sortie de la portée de la fonction. Est-ce que je néglige quelque chose?
Juste pour clarifier, après avoir lu plusieurs réponses: je suis parfaitement d'accord sur les préoccupations d'optimisation prématurée, et j'essaie toujours de profiler d'abord puis de travailler sur les hotspots. Ma question portait davantage sur un code purement technique, si vous voyez ce que je veux dire.
Réponses:
Le but d'une
shared_ptr
instance distincte est de garantir (dans la mesure du possible) que tant que celashared_ptr
est dans la portée, l'objet vers lequel elle pointe existera toujours, car son nombre de références sera au moins égal à 1.Ainsi, en utilisant une référence à a
shared_ptr
, vous désactivez cette garantie. Donc dans votre deuxième cas:Comment savez-vous que
sp->do_something()
cela ne va pas exploser en raison d'un pointeur nul?Tout dépend de ce qu'il y a dans ces sections «...» du code. Que faire si vous appelez quelque chose pendant le premier «...» qui a pour effet secondaire (quelque part dans une autre partie du code) d'effacer un
shared_ptr
sur ce même objet? Et si c'était le seul restant distinctshared_ptr
de cet objet? Bye bye object, juste là où vous êtes sur le point d'essayer de l'utiliser.Il y a donc deux façons de répondre à cette question:
Examinez très attentivement la source de l'ensemble de votre programme jusqu'à ce que vous soyez sûr que l'objet ne mourra pas pendant le corps de la fonction.
Remplacez le paramètre par un objet distinct au lieu d'une référence.
Conseil général qui s'applique ici: ne vous souciez pas d'apporter des modifications risquées à votre code pour des raisons de performances jusqu'à ce que vous ayez chronométré votre produit dans une situation réaliste dans un profileur et que vous ayez mesuré de manière concluante que le changement que vous souhaitez apporter fera un différence significative de performance.
Mise à jour pour le commentateur JQ
Voici un exemple artificiel. C'est délibérément simple, donc l'erreur sera évidente. Dans les exemples réels, l'erreur n'est pas si évidente car elle est cachée dans des couches de détails réels.
Nous avons une fonction qui enverra un message quelque part. Il peut s'agir d'un message volumineux, donc plutôt que d'utiliser un
std::string
qui est probablement copié lorsqu'il est transmis à plusieurs endroits, nous utilisons unshared_ptr
dans une chaîne:(Nous l '«envoyons» simplement à la console pour cet exemple).
Maintenant, nous voulons ajouter une fonction pour se souvenir du message précédent. Nous voulons le comportement suivant: il doit exister une variable contenant le message le plus récemment envoyé, mais pendant qu'un message est en cours d'envoi, il ne doit y avoir aucun message précédent (la variable doit être réinitialisée avant l'envoi). Nous déclarons donc la nouvelle variable:
Ensuite, nous modifions notre fonction selon les règles que nous avons spécifiées:
Ainsi, avant de commencer à envoyer, nous rejetons le message précédent actuel, puis une fois l'envoi terminé, nous pouvons stocker le nouveau message précédent. Tout bon. Voici un code de test:
Et comme prévu, cela s'imprime
Hi!
deux fois.Maintenant vient M. Maintainer, qui regarde le code et pense: Hé, ce paramètre
send_message
est unshared_ptr
:Évidemment, cela peut être changé en:
Pensez à l'amélioration des performances que cela apportera! (Peu importe que nous soyons sur le point d'envoyer un message généralement volumineux sur un canal, l'amélioration des performances sera donc si petite qu'elle ne sera pas mesurable).
Mais le vrai problème est que le code de test présentera désormais un comportement indéfini (dans les versions de débogage Visual C ++ 2010, il se bloque).
M. Maintainer est surpris par cela, mais ajoute un test défensif pour
send_message
tenter d'arrêter le problème:Mais bien sûr, il continue et plante, car il
msg
n'est jamais nul lorsqu'ilsend_message
est appelé.Comme je l'ai dit, avec tout le code si rapproché dans un exemple trivial, il est facile de trouver l'erreur. Mais dans les programmes réels, avec des relations plus complexes entre des objets mutables qui détiennent des pointeurs les uns vers les autres, il est facile de faire l'erreur et difficile de construire les cas de test nécessaires pour détecter l'erreur.
La solution simple, où vous voulez qu'une fonction puisse s'appuyer sur un
shared_ptr
continu à être non nul partout, est que la fonction alloue son propre vraishared_ptr
, plutôt que de s'appuyer sur une référence à un existantshared_ptr
.L'inconvénient est que a copié
shared_ptr
n'est pas gratuit: même les implémentations "sans verrouillage" doivent utiliser une opération interlocked pour honorer les garanties de thread. Il peut donc y avoir des situations où un programme peut être considérablement accéléré en changeant ashared_ptr
en ashared_ptr &
. Mais ce n'est pas un changement qui peut être apporté en toute sécurité à tous les programmes. Cela change la signification logique du programme.Notez qu'un bug similaire se produirait si nous utilisions
std::string
partout au lieu destd::shared_ptr<std::string>
et au lieu de:pour effacer le message, nous avons dit:
Le symptôme serait alors l'envoi accidentel d'un message vide, au lieu d'un comportement indéfini. Le coût d'une copie supplémentaire d'une très grande chaîne peut être beaucoup plus important que le coût de copie d'un
shared_ptr
, donc le compromis peut être différent.la source
Je me suis retrouvé en désaccord avec la réponse la plus votée, alors j'ai cherché des opinions d'experts et les voici. De http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything
Herb Sutter: "lorsque vous passez shared_ptrs, les copies coûtent cher"
Scott Meyers: "Shared_ptr n'a rien de spécial quand il s'agit de savoir si vous le transmettez par valeur ou par référence. Utilisez exactement la même analyse que vous utilisez pour tout autre type défini par l'utilisateur. Les gens semblent avoir cette perception que shared_ptr résout en quelque sorte tous les problèmes de gestion, et que parce que c'est petit, il est forcément peu coûteux de passer par valeur. Il faut le copier, et il y a un coût associé à ça ... c'est cher de le passer par valeur, donc si je peux m'en tirer avec une sémantique appropriée dans mon programme, je vais le passer par référence à const ou référence à la place "
Herb Sutter: "passez-les toujours par référence à const, et très occasionnellement peut-être parce que vous savez ce que vous avez appelé pourrait modifier la chose dont vous avez obtenu une référence, peut-être que vous pourriez alors passer par valeur ... si vous les copiez en tant que paramètres, oh mon Dieu, vous n’avez presque jamais besoin d’augmenter ce nombre de références car il est de toute façon maintenu en vie, et vous devriez le passer par référence, alors faites-le. "
Mise à jour: Herb a développé ceci ici: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ , bien que la morale de l'histoire soit que vous ne devriez pas passer shared_ptrs at all "à moins que vous ne souhaitiez utiliser ou manipuler le pointeur intelligent lui-même, par exemple pour partager ou transférer la propriété."
la source
shared_ptr
s, il n'y avait pas d'accord sur la question du passage par valeur par rapport à la référence.const &
affectera la sémantique n'est facile que dans très programmes simples.Je déconseillerais cette pratique à moins que vous et les autres programmeurs avec lesquels vous travaillez vraiment, vraiment savoir ce que vous faites tous.
Premièrement, vous n'avez aucune idée de la façon dont l'interface avec votre classe pourrait évoluer et vous voulez empêcher les autres programmeurs de faire de mauvaises choses. Passer un shared_ptr par référence n'est pas quelque chose qu'un programmeur devrait s'attendre à voir, car ce n'est pas idiomatique, et cela facilite son utilisation incorrecte. Programmez de manière défensive: rendez l'interface difficile à utiliser de manière incorrecte. Passer par référence ne fera qu'inviter des problèmes plus tard.
Deuxièmement, n'optimisez pas jusqu'à ce que vous sachiez que cette classe particulière va être un problème. Profil d'abord, et ensuite si votre programme a vraiment besoin du coup de pouce donné en passant par référence, alors peut-être. Sinon, ne vous inquiétez pas pour les petites choses (c'est-à-dire les N instructions supplémentaires qu'il faut pour passer par valeur) au lieu de vous soucier de la conception, des structures de données, des algorithmes et de la maintenabilité à long terme.
la source
Oui, prendre une référence est bien là-bas. Vous n'avez pas l'intention de partager la propriété de la méthode; il veut seulement travailler avec. Vous pouvez également prendre une référence pour le premier cas, puisque vous le copiez quand même. Mais pour le premier cas, il faut la propriété. Il y a cette astuce pour le copier encore une seule fois:
Vous devez également copier lorsque vous le renvoyez (c'est-à-dire ne pas renvoyer une référence). Parce que votre classe ne sait pas ce que le client en fait (elle pourrait stocker un pointeur vers elle et puis un big bang se produira). S'il s'avère plus tard qu'il s'agit d'un goulot d'étranglement (premier profil!), Vous pouvez toujours renvoyer une référence.
Edit : Bien sûr, comme d'autres le soulignent, cela n'est vrai que si vous connaissez votre code et savez que vous ne réinitialisez pas le pointeur partagé transmis d'une manière ou d'une autre. En cas de doute, passez simplement par valeur.
la source
Il est judicieux de passer
shared_ptr
sconst&
. Cela ne causera probablement pas de problèmes (sauf dans le cas peu probable où le référencéshared_ptr
est supprimé pendant l'appel de fonction, comme détaillé par Earwicker) et ce sera probablement plus rapide si vous en passez beaucoup. Rappelles toi; la valeur par défautboost::shared_ptr
est thread safe, donc sa copie inclut un incrément thread safe.Essayez d'utiliser
const&
plutôt que simplement&
, car les objets temporaires ne peuvent pas être passés par référence non const. (Même si une extension de langue dans MSVC vous permet de le faire de toute façon)la source
Dans le second cas, cela est plus simple:
Vous pouvez l'appeler comme
la source
J'éviterais une référence "simple" à moins que la fonction ne modifie explicitement le pointeur.
A
const &
peut être une micro-optimisation sensible lors de l'appel de petites fonctions - par exemple pour permettre d'autres optimisations, comme l'inclusion de certaines conditions. De plus, l'incrémentation / décrémentation - puisqu'elle est thread-safe - est un point de synchronisation. Cependant, je ne m'attendrais pas à ce que cela fasse une grande différence dans la plupart des scénarios.En règle générale, vous devez utiliser le style le plus simple, sauf si vous avez des raisons de ne pas le faire. Ensuite, utilisez le de manière
const &
cohérente ou ajoutez un commentaire expliquant pourquoi si vous ne l'utilisez qu'à quelques endroits.la source
Je recommanderais de passer un pointeur partagé par référence const - une sémantique selon laquelle la fonction passée avec le pointeur ne possède PAS le pointeur, qui est un idiome propre pour les développeurs.
Le seul inconvénient est que dans plusieurs programmes de thread, l'objet pointé par le pointeur partagé est détruit dans un autre thread. Il est donc prudent de dire que l'utilisation de la référence const du pointeur partagé est sûre dans un programme à thread unique.
Passer un pointeur partagé par une référence non-const est parfois dangereux - la raison en est les fonctions d'échange et de réinitialisation que la fonction peut invoquer à l'intérieur afin de détruire l'objet qui est toujours considéré comme valide après le retour de la fonction.
Il ne s'agit pas d'optimisation prématurée, je suppose - il s'agit d'éviter le gaspillage inutile de cycles de processeur lorsque vous savez clairement ce que vous voulez faire et que l'idiome de codage a été fermement adopté par vos collègues développeurs.
Juste mes 2 cents :-)
la source
Il semble que tous les avantages et inconvénients ici peuvent en fait être généralisés à N'IMPORTE QUEL type passé par référence et pas seulement shared_ptr. À mon avis, vous devez connaître la sémantique du passage par référence, référence const et valeur et l'utiliser correctement. Mais il n'y a absolument rien de mal en soi à passer shared_ptr par référence, à moins que vous ne pensiez que toutes les références sont mauvaises ...
Pour revenir à l'exemple:
Comment sais-tu ça
sp.do_something()
cela ne va pas exploser à cause d'un pointeur suspendu?La vérité est que, shared_ptr ou non, const ou non, cela peut se produire si vous avez un défaut de conception, comme le partage direct ou indirect de la propriété
sp
entre les threads, l'utilisation incorrecte d'un objetdelete this
, vous avez une propriété circulaire ou d'autres erreurs de propriété.la source
Une chose que je n'ai pas encore vue mentionnée est que lorsque vous passez des pointeurs partagés par référence, vous perdez la conversion implicite que vous obtenez si vous souhaitez passer un pointeur partagé de classe dérivée via une référence à un pointeur partagé de classe de base.
Par exemple, ce code produira une erreur, mais il fonctionnera si vous modifiez
test()
afin que le pointeur partagé ne soit pas passé par référence.la source
Je suppose que vous êtes familier avec l' optimisation prématurée et demandez à des fins académiques ou parce que vous avez isolé un code préexistant qui est sous-performant.
Passer par référence, c'est bien
Passer par référence const est meilleur et peut généralement être utilisé, car il ne force pas la const-ness sur l'objet pointé.
Vous ne risquez pas de perdre le pointeur en raison de l'utilisation d'une référence. Cette référence prouve que vous avez une copie du pointeur intelligent plus tôt dans la pile et qu'un seul thread possède une pile d'appels, de sorte que la copie préexistante ne disparaît pas.
L'utilisation de références est souvent plus efficace pour les raisons que vous mentionnez, mais pas garantie . N'oubliez pas que déréférencer un objet peut également demander du travail. Votre scénario idéal d'utilisation de référence serait si votre style de codage implique de nombreuses petites fonctions, où le pointeur serait passé de fonction en fonction à fonction avant d'être utilisé.
Vous devez toujours éviter de stocker votre pointeur intelligent comme référence. Votre
Class::take_copy_of_sp(&sp)
exemple montre une utilisation correcte pour cela.la source
En supposant que nous ne soyons pas concernés par l'exactitude de la const (ou plus, vous voulez permettre aux fonctions de pouvoir modifier ou partager la propriété des données transmises), transmettre un boost :: shared_ptr par valeur est plus sûr que de le passer par référence comme nous permettons à l'original boost :: shared_ptr de contrôler sa propre durée de vie. Considérez les résultats du code suivant ...
Dans l'exemple ci-dessus, nous voyons que le passage de sharedA par référence permet à FooTakesReference de réinitialiser le pointeur d'origine, ce qui réduit son nombre d'utilisation à 0, détruisant ses données. FooTakesValue , cependant, ne peut pas réinitialiser le pointeur d'origine, garantissant que les données de sharedB sont toujours utilisables. Quand un autre développeur arrive inévitablement et tente de s'appuyer sur l' existence fragile de sharedA , le chaos s'ensuit. Cependant, l' heureux développeur sharedB rentre chez lui tôt car tout va bien dans son monde.
La sécurité du code, dans ce cas, l'emporte de loin sur toute amélioration de la vitesse créée par la copie. En même temps, le boost :: shared_ptr est destiné à améliorer la sécurité du code. Il sera beaucoup plus facile de passer d'une copie à une référence, si quelque chose nécessite ce genre d'optimisation de niche.
la source
Sandy a écrit: "Il semble que tous les avantages et inconvénients ici peuvent en fait être généralisés à N'IMPORTE QUEL type passé par référence et pas seulement shared_ptr."
C'est vrai dans une certaine mesure, mais le but d'utiliser shared_ptr est d'éliminer les problèmes concernant la durée de vie des objets et de laisser le compilateur gérer cela pour vous. Si vous allez passer un pointeur partagé par référence et permettre aux clients de votre objet de référence d'appeler des méthodes non const qui pourraient libérer les données de l'objet, alors l'utilisation d'un pointeur partagé est presque inutile.
J'ai écrit «presque» dans cette phrase précédente parce que les performances peuvent être un problème, et cela «pourrait» être justifié dans de rares cas, mais j'éviterais également ce scénario moi-même et chercherais moi-même toutes les autres solutions d'optimisation possibles, comme regarder sérieusement à l'ajout d'un autre niveau d'indirection, d'évaluation paresseuse, etc.
Le code qui existe au-delà de son auteur, ou même après sa mémoire de l'auteur, qui nécessite des hypothèses implicites sur le comportement, en particulier le comportement sur la durée de vie des objets, nécessite une documentation claire, concise et lisible, et puis de nombreux clients ne le liront pas de toute façon! La simplicité l'emporte presque toujours sur l'efficacité, et il existe presque toujours d'autres moyens d'être efficace. Si vous avez vraiment besoin de passer des valeurs par référence pour éviter la copie profonde par des constructeurs de copie de vos objets comptés de référence (et de l'opérateur égal), vous devriez peut-être envisager des moyens de faire des données copiées en profondeur des pointeurs comptés par référence qui peuvent être copié rapidement. (Bien sûr, ce n'est qu'un scénario de conception qui pourrait ne pas s'appliquer à votre situation).
la source
J'avais l'habitude de travailler dans un projet dont le principe était très fort de transmettre des pointeurs intelligents par valeur. Lorsqu'on m'a demandé de faire une analyse des performances - j'ai constaté que pour l'incrémentation et la décrémentation des compteurs de référence des pointeurs intelligents, l'application passe entre 4 et 6% du temps de processeur utilisé.
Si vous voulez passer les pointeurs intelligents par valeur juste pour éviter d'avoir des problèmes dans des cas étranges comme décrit par Daniel Earwicker, assurez-vous de comprendre le prix que vous payez pour cela.
Si vous décidez d'utiliser une référence, la principale raison d'utiliser la référence const est de rendre possible la conversion ascendante implicite lorsque vous devez transmettre un pointeur partagé à un objet de la classe qui hérite de la classe que vous utilisez dans l'interface.
la source
En plus de ce que litb a dit, j'aimerais souligner que c'est probablement pour passer par référence const dans le deuxième exemple, de cette façon vous êtes sûr de ne pas le modifier accidentellement.
la source
passer par valeur:
passer par référence:
passer par pointeur:
la source
Chaque morceau de code doit avoir un sens. Si vous passez un pointeur partagé par valeur partout dans l'application, cela signifie "Je ne suis pas sûr de ce qui se passe ailleurs, donc je privilégie la sécurité brute ". Ce n'est pas ce que j'appelle un bon signe de confiance pour les autres programmeurs qui pourraient consulter le code.
Quoi qu'il en soit, même si une fonction obtient une référence const et que vous n'êtes "pas sûr", vous pouvez toujours créer une copie du pointeur partagé en tête de la fonction, pour ajouter une référence forte au pointeur. Cela pourrait également être vu comme un indice sur la conception ("le pointeur pourrait être modifié ailleurs").
Alors oui, OMI, la valeur par défaut devrait être " passer par référence const ".
la source