web-dev-qa-db-fra.com

Passage de const shared_ptr <T> & contre juste shared_ptr <T> comme paramètre

J'ai lu un certain nombre de discussions sur les problèmes de performances lorsque des pointeurs intelligents sont impliqués dans une application. L'une des recommandations fréquentes consiste à passer un pointeur intelligent en tant que const & au lieu d'une copie, comme ceci:

void doSomething(std::shared_ptr<T> o) {}

versus

void doSomething(const std::shared_ptr<T> &o) {}

Cependant, la deuxième variante ne va-t-elle pas à l'encontre du but d'un pointeur partagé? Nous partageons en fait le pointeur partagé ici, donc si, pour certaines raisons, le pointeur est publié dans le code appelant (pensez à la réentrance ou aux effets secondaires), le pointeur const devient invalide. Une situation que le pointeur partagé devrait réellement empêcher. Je comprends que const & fait gagner du temps car il n'y a pas de copie impliquée et pas de verrouillage pour gérer le nombre de références. Mais le prix rend le code moins sûr, non?

24
Mike Lischke

L'avantage de passer le shared_ptr par const& signifie que le nombre de références ne doit pas être augmenté puis diminué. Parce que ces opérations doivent être thread-safe, elles peuvent être coûteuses.

Vous avez tout à fait raison, il existe un risque que vous puissiez avoir une chaîne de passes par référence qui invalide plus tard la tête de la chaîne. Cela m'est arrivé une fois dans un projet du monde réel avec des conséquences réelles. Une fonction a trouvé un shared_ptr dans un conteneur et lui a transmis une référence dans une pile d'appels. Une fonction au fond de la pile d'appels a supprimé l'objet du conteneur, provoquant soudainement toutes les références à un objet qui n'existait plus.

Ainsi, lorsque vous passez quelque chose par référence, l'appelant doit s'assurer qu'il survit pendant toute la durée de l'appel de fonction. N'utilisez pas une passe par référence si c'est un problème.

(Je suppose que vous avez un cas d'utilisation où il y a une raison spécifique de passer par shared_ptr plutôt que par référence. La raison la plus courante serait que la fonction appelée peut avoir besoin de prolonger la durée de vie de l'objet.)

Mise à jour: Quelques détails supplémentaires sur le bogue pour les personnes intéressées: Ce programme avait des objets partagés et implémentait la sécurité des threads internes. Ils étaient détenus dans des conteneurs et il était courant que les fonctions prolongent leur durée de vie.

Ce type particulier d'objet pourrait vivre dans deux conteneurs. Un quand il était actif et un quand il était inactif. Certaines opérations ont fonctionné sur des objets actifs, d'autres sur des objets inactifs. Le cas d'erreur s'est produit lorsqu'une commande a été reçue sur un objet inactif qui l'a rendu actif alors que le seul shared_ptr à l'objet était détenu par le conteneur d'objets inactifs.

L'objet inactif était situé dans son conteneur. Une référence au shared_ptr dans le conteneur a été transmis, par référence, au gestionnaire de commandes. Grâce à une chaîne de références, cette shared_ptr a finalement obtenu le code qui a réalisé qu'il s'agissait d'un objet inactif qui devait être rendu actif. L'objet a été retiré du conteneur inactif (ce qui a détruit le conteneur inactif shared_ptr) et ajouté au conteneur actif (qui a ajouté une autre référence au shared_ptr passé à la routine "add").

À ce stade, il était possible que le seul shared_ptr à l'objet qui existait était celui du conteneur inactif. Toutes les autres fonctions de la pile d'appels y avaient simplement une référence. Lorsque l'objet a été supprimé du conteneur inactif, l'objet a pu être détruit et toutes ces références se rapportaient à un shared_ptr que cela n'existait plus.

Il a fallu environ un mois pour démêler cela.

21
David Schwartz

Tout d'abord, ne passez pas un shared_ptr dans une chaîne d'appels, sauf s'il est possible qu'une des fonctions appelées en stocke une copie. Passez une référence à l'objet référencé, ou un pointeur brut vers cet objet, ou éventuellement une boîte, selon qu'il peut être facultatif ou non.

Mais quand vous passez un shared_ptr, puis passez-le de préférence par référence à const, car la copie d'un shared_ptr a des frais supplémentaires. La copie doit mettre à jour le nombre de références partagées et cette mise à jour doit être thread-safe. Il y a donc un peu d'inefficacité qui peut être évitée (en toute sécurité).

En ce qui concerne

le prix rend le code moins sûr, non?

Non. Le prix est une indirection supplémentaire dans le code machine généré naïvement, mais le compilateur gère cela. Il s'agit donc simplement d'éviter un surcoût mineur mais totalement inutile que le compilateur ne peut pas optimiser pour vous, à moins qu'il ne soit super intelligent.

Comme David Schwarz l'a illustré dans sa réponse, lorsque vous passez par référence à const le problème d'alias, où la fonction que vous appelez à son tour change ou appelle une fonction qui change l'objet d'origine, est possible. Et selon la loi de Murphy, cela se produira au moment le plus gênant, au coût maximum, et avec le code impénétrable le plus compliqué. Mais c'est le cas, que l'argument soit un string ou un shared_ptr ou peu importe. Heureusement, c'est un problème très rare. Mais gardez cela à l'esprit, également pour passer shared_ptr les instances.

12

Tout d'abord, il existe une différence sémantique entre les deux: le passage d'un pointeur partagé par valeur indique que votre fonction va prendre sa part de la propriété de l'objet sous-jacent. Passer shared_ptr en tant que référence const n'indique aucune intention en plus de simplement passer l'objet sous-jacent par référence const (ou pointeur brut) en dehors de forcer les utilisateurs de cette fonction à utiliser shared_ptr. Donc surtout des ordures.

La comparaison des implications de performance de celles-ci est sans pertinence tant qu'elles sont sémantiquement différentes.

de https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/

Ne transmettez pas un pointeur intelligent en tant que paramètre de fonction, sauf si vous souhaitez utiliser ou manipuler le pointeur intelligent lui-même, par exemple pour partager ou transférer la propriété.

et cette fois je suis totalement d'accord avec Herb :)

Et une autre citation du même, qui répond plus directement à la question

Ligne directrice: Utilisez un paramètre & const partagé non-const uniquement pour modifier le shared_ptr. N'utilisez un const shared_ptr & en tant que paramètre que si vous n'êtes pas sûr de prendre ou non une copie et de partager la propriété; sinon, utilisez * à la place (ou si non annulable, a &)

5
Slava

Comme indiqué dans C++ - shared_ptr: vitesse horrible , copier un shared_ptr prend du temps. La construction implique un incrément atomique et la destruction un décrément atomique, une mise à jour atomique (incrément ou décrément) peut empêcher un certain nombre d'optimisations du compilateur (les charges/magasins de mémoire ne peuvent pas migrer à travers l'opération) et au niveau matériel implique le protocole de cohérence du cache du CPU pour s'assurer que toute la ligne de cache appartient (mode exclusif) au noyau effectuant la modification.

Alors, tu as raison, std::shared_ptr<T> const& peut être utilisé comme une amélioration des performances par rapport à std::shared_ptr<T>.


Vous avez également raison de penser qu'il existe un risque théorique que le pointeur/la référence se balance pendant un certain aliasing.

Cela étant dit, le risque est déjà latent dans tout programme C++: toute utilisation unique d'un pointeur ou d'une référence est un risque. Je dirais que les quelques occurrences de std::shared_ptr<T> const& devrait être une goutte d'eau par rapport au nombre total d'utilisations de T&, T const&, T*, ...


Enfin, je voudrais souligner que le passage d'un shared_ptr<T> const& est bizarre . Les cas suivants sont courants:

  • shared_ptr<T>: J'ai besoin d'une copie du shared_ptr
  • T*/T const&/T&/T const&: J'ai besoin d'un handle (éventuellement nul) pour T

Le cas suivant est beaucoup moins courant:

  • shared_ptr<T>&: Je peux réinstaller le shared_ptr

Mais en passant shared_ptr<T> const&? Les utilisations légitimes sont très très rares.

Qui passe shared_ptr<T> const& où tout ce que vous voulez est une référence à T est un anti-modèle: vous forcez l'utilisateur à utiliser shared_ptr quand ils pourraient allouer T d'une autre manière! La plupart du temps (99,99 ..%), vous ne devez pas vous soucier de la façon dont T est alloué.

Le seul cas où vous passeriez un shared_ptr<T> const& est si vous n'êtes pas sûr d'avoir besoin d'une copie ou non, et parce que vous avez profilé le programme et montré que cet incrément/décrément atomique était un goulot d'étranglement, vous avez décidé de ne reporter la création de la copie qu'aux cas où c'est nécessaire.

Il s'agit d'un tel cas Edge que toute utilisation de shared_ptr<T> const& doit être considéré avec le plus haut degré de suspicion.

3
Matthieu M.

Je pense qu'il est parfaitement raisonnable de passer par const & si la fonction cible est synchrone et n'utilise le paramètre que lors de l'exécution et n'en a plus besoin lors du retour. Ici, il est raisonnable d'économiser sur le coût de l'augmentation du nombre de références - car vous n'avez pas vraiment besoin de la sécurité supplémentaire dans ces circonstances limitées - à condition de comprendre les implications et d'être sûr que le code est sûr.

C'est par opposition au moment où la fonction doit enregistrer le paramètre (par exemple dans un membre de classe) pour une référence ultérieure.

3
Smeeheey

Si aucune modification de propriété n'est impliquée dans votre méthode, il n'y a aucun avantage pour votre méthode à prendre un shared_ptr par copie ou par référence const, cela pollue l'API et peut entraîner des frais généraux (si vous passez par copie)

La meilleure façon est de passer le type sous-jacent par const ref ou ref selon votre cas d'utilisation

void doSomething(const T& o) {}

auto s = std::make_shared<T>(...);
// ...
doSomething(*s);

Le pointeur sous-jacent ne peut pas être libéré lors de l'appel de méthode

3
Stephane Molina