Passer des pointeurs partagés comme arguments

88

Si je déclare un objet enveloppé dans un pointeur partagé:

std::shared_ptr<myClass> myClassObject(new myClass());

alors j'ai voulu le passer comme argument à une méthode:

DoSomething(myClassObject);

//the called method
void DoSomething(std::shared_ptr<myClass> arg1)
{
   arg1->someField = 4;
}

Est-ce que ce qui précède incrémente simplement le nombre de références de shared_pt et tout est cool? Ou laisse-t-il un pointeur suspendu?

Êtes-vous toujours censé le faire?:

DoSomething(myClassObject.Get());

void DoSomething(std::shared_ptr<myClass>* arg1)
{
   (*arg1)->someField = 4;
}

Je pense que la 2ème manière peut être plus efficace car elle n'a qu'à copier 1 adresse (par opposition à l'ensemble du pointeur intelligent), mais la 1ère manière semble plus lisible et je ne prévois pas de repousser les limites de performances. Je veux juste m'assurer qu'il n'y a pas quelque chose de dangereux à ce sujet.

Merci.

Steve H
la source
14
const std::shared_ptr<myClass>& arg1
Captain Obvlious
3
La deuxième façon est cassée, la première est idiomatique si vous avez réellement besoin de votre fonction pour partager la propriété. Mais faut-il DoSomethingvraiment partager la propriété? Il semble que cela devrait simplement prendre une référence à la place ...
ildjarn
8
@SteveH: Ce n'est pas le cas, mais pourquoi votre fonction devrait-elle forcer une sémantique spéciale de propriété d'objets sur ses appelants si elle n'en a pas réellement besoin? Votre fonction ne fait rien qui ne serait pas mieux void DoSomething(myClass& arg1).
ildjarn
2
@SteveH: Le but entier des pointeurs intelligents est de gérer les problèmes de propriété d'objets inhabituels - si vous ne les avez pas, vous ne devriez pas utiliser de pointeurs intelligents en premier lieu. ; -] En shared_ptr<>particulier, vous devez passer par valeur pour réellement partager la propriété.
ildjarn
4
Sans rapport: en général, std::make_sharedest non seulement plus efficace, mais aussi plus sûr que le std::shared_ptrconstructeur.
R. Martinho Fernandes

Réponses:

171

Je souhaite transmettre un pointeur partagé à une fonction. Pouvez-vous m'aider?

Bien sûr, je peux vous aider. Je suppose que vous avez une certaine compréhension de la sémantique de propriété en C ++. Est-ce vrai?

Ouais, je suis assez à l'aise avec le sujet.

Bien.

Ok, je ne peux penser qu'à deux raisons de prendre un shared_ptrargument:

  1. La fonction veut partager la propriété de l'objet;
  2. La fonction effectue une opération qui fonctionne spécifiquement sur shared_ptrs.

Laquelle vous intéresse?

Je cherche une réponse générale, donc je m'intéresse aux deux. Je suis curieux de savoir ce que vous voulez dire dans le cas n ° 2, cependant.

Des exemples de telles fonctions incluent std::static_pointer_castdes comparateurs personnalisés ou des prédicats. Par exemple, si vous avez besoin de trouver tous les shared_ptr uniques à partir d'un vecteur, vous avez besoin d'un tel prédicat.

Ah, quand la fonction a réellement besoin de manipuler le pointeur intelligent lui-même.

Exactement.

Dans ce cas, je pense que nous devrions passer par référence.

Oui. Et si cela ne change pas le pointeur, vous voulez passer par référence const. Il n'est pas nécessaire de copier puisque vous n'avez pas besoin de partager la propriété. C'est l'autre scénario.

Ok, compris. Parlons de l'autre scénario.

Celui dont vous partagez la propriété? D'accord. Avec quoi partagez-vous la propriété shared_ptr?

En le copiant.

Ensuite, la fonction devra faire une copie d'un shared_ptr, correct?

Évidemment. Alors je le passe par une référence à const et je le copie dans une variable locale?

Non, c'est une pessimisation. S'il est passé par référence, la fonction n'aura d'autre choix que de faire la copie manuellement. S'il est passé par valeur, le compilateur choisira le meilleur choix entre une copie et un déplacement et l'exécutera automatiquement. Alors, passez par valeur.

Bon point. Je dois me rappeler plus souvent cet article " Vous voulez de la vitesse? Passez par la valeur. "

Attendez, que se passe-t-il si la fonction stocke le shared_ptrdans une variable membre, par exemple? Cela ne fera-t-il pas une copie redondante?

La fonction peut simplement déplacer l' shared_ptrargument dans son stockage. Déplacer un shared_ptrest bon marché car cela ne change aucun nombre de références.

Ah, bonne idée.

Mais je pense à un troisième scénario: et si vous ne voulez pas manipuler le shared_ptr, ni partager la propriété?

Dans ce cas, cela shared_ptrn'a aucun rapport avec la fonction. Si vous souhaitez manipuler la pointee, prenez une pointee et laissez les appelants choisir la sémantique de propriété qu'ils souhaitent.

Et dois-je prendre la pointee par référence ou par valeur?

Les règles habituelles s'appliquent. Les pointeurs intelligents ne changent rien.

Passez par valeur si je vais copier, passez par référence si je veux éviter une copie.

Droite.

Hmm. Je pense que vous avez oublié un autre scénario. Et si je veux partager la propriété, mais uniquement en fonction d'une certaine condition?

Ah, un cas de pointe intéressant. Je ne m'attends pas à ce que cela arrive souvent. Mais quand cela se produit, vous pouvez soit passer par valeur et ignorer la copie si vous n'en avez pas besoin, soit passer par référence et faire la copie si vous en avez besoin.

Je risque une copie redondante dans la première option et perd un mouvement potentiel dans la seconde. Je ne peux pas manger le gâteau et l'avoir aussi?

Si vous êtes dans une situation où cela compte vraiment, vous pouvez fournir deux surcharges, l'une prenant une référence const lvalue et l'autre prenant une référence rvalue. L'un copie, l'autre bouge. Un modèle de fonction de transfert parfait est une autre option.

Je pense que cela couvre tous les scénarios possibles. Merci beaucoup.

R. Martinho Fernandes
la source
2
@Jon: Pour quoi faire? Ceci est tout au sujet dois - je faire A ou dois - je faire B . Je pense que nous savons tous comment passer des objets par valeur / référence, non?
sbi
9
Parce qu'une prose comme "Est-ce que ça veut dire que je vais passer par une référence à const pour faire la copie? Non, c'est une pessimisation" est déroutante pour un débutant. Passer par une référence à const ne fait pas de copie.
Jon
1
@Martinho Je ne suis pas d'accord avec la règle "Si vous voulez manipuler la pointee, prenez une pointee". Comme indiqué dans cette réponse, ne pas prendre temporairement possession d'un objet peut être dangereux. À mon avis, la valeur par défaut devrait être de transmettre une copie pour la propriété temporaire afin de garantir la validité de l'objet pour la durée de l'appel de fonction.
radman
@radman Aucun scénario dans la réponse à laquelle vous créez un lien n'applique cette règle, donc elle n'a aucune importance. S'il vous plaît écrivez-moi un SSCCE où vous passez par référence de a shared_ptr<T> ptr;(c'est void f(T&)-à- dire appelé avec f(*ptr)) et l'objet ne survit pas à l'appel. Sinon, écrivez-en un où vous passez par valeur void f(T)et où des problèmes surviennent.
R. Martinho Fernandes
@Martinho consultez mon exemple de code pour le simple exemple correct et autonome. L'exemple est pour void f(T&)et implique des threads. Je pense que mon problème est que le ton de votre réponse décourage de passer la propriété de shared_ptr, qui est la manière la plus sûre, la plus intuitive et la moins compliquée de les utiliser. Je serais extrêmement contre de conseiller jamais à un débutant d'extraire une référence à des données appartenant à un shared_ptr <> les possibilités d'utilisation abusive sont grandes et l'avantage est minime.
radman
22

Je pense que les gens ont inutilement peur d'utiliser des pointeurs bruts comme paramètres de fonction. Si la fonction ne va pas stocker le pointeur ou affecter sa durée de vie, un pointeur brut fonctionne tout aussi bien et représente le plus petit dénominateur commun. Considérez par exemple comment vous passeriez a unique_ptrdans une fonction qui prend a shared_ptrcomme paramètre, soit par valeur, soit par référence const?

void DoSomething(myClass * p);

DoSomething(myClass_shared_ptr.get());
DoSomething(myClass_unique_ptr.get());

Un pointeur brut en tant que paramètre de fonction ne vous empêche pas d'utiliser des pointeurs intelligents dans le code appelant, là où cela compte vraiment.

Mark Ransom
la source
8
Si vous utilisez un pointeur, pourquoi ne pas simplement utiliser une référence? DoSomething(*a_smart_ptr)
Xeo
5
@Xeo, vous avez raison - ce serait encore mieux. Parfois, vous devez toutefois prévoir la possibilité d'un pointeur NULL.
Mark Ransom
1
Si vous avez une convention d'utilisation de pointeurs bruts comme paramètres de sortie, il est plus facile de repérer dans le code d'appel qu'une variable peut changer, par exemple: comparer fun(&x)à fun(x). Dans ce dernier exemple, il xpeut être passé par valeur ou par const ref. Comme indiqué ci-dessus, cela vous permettra également de passer nullptrsi vous n'êtes pas intéressé par la sortie ...
Andreas Magnusson
2
@AndreasMagnusson: Cette convention de pointeur en tant que paramètre de sortie peut donner un faux sentiment de sécurité lorsqu'il s'agit de pointeurs passés d'une autre source. Parce que dans ce cas, l'appel est amusant (x) et * x est modifié et la source n'a pas de & x pour vous avertir.
Zan Lynx
Et si l'appel de fonction survit à la portée de l'appelant? Il est possible que le destructeur du ptr partagé ait été appelé, et maintenant la fonction tentera d'accéder à la mémoire supprimée, ce qui entraînera UB
Sridhar Thiagarajan
4

Oui, l'idée même d'un shared_ptr <> est que plusieurs instances peuvent contenir le même pointeur brut et la mémoire sous-jacente ne sera libérée que lorsque la dernière instance de shared_ptr <> sera détruite.

J'éviterais un pointeur vers un shared_ptr <> car cela va à l'encontre de l'objectif car vous avez à nouveau affaire à raw_pointers.

R Samuel Klatchko
la source
2

Le passage par valeur dans votre premier exemple est sûr, mais il y a un meilleur idiome. Passez par référence const lorsque cela est possible - je dirais oui même lorsque vous traitez avec des pointeurs intelligents. Votre deuxième exemple n'est pas exactement cassé mais il est très !???. Idiot, ne rien accomplir et vaincre une partie de l'intérêt des pointeurs intelligents, et vous laisser dans un monde bogué de douleur lorsque vous essayez de déréférencer et de modifier les choses.

Djechlin
la source
3
Personne ne préconise de passer par un pointeur, si vous voulez dire un pointeur brut. Passer par référence s'il n'y a pas de conflit de propriété est certainement idiomatique. Le point est, en ce shared_ptr<>qui concerne spécifiquement, que vous devez faire une copie afin de réellement partager la propriété; si vous avez une référence ou un pointeur vers a, shared_ptr<>vous ne partagez rien et êtes soumis aux mêmes problèmes de durée de vie qu'une référence ou un pointeur normal.
ildjarn
2
@ildjarn: Passer une référence constante est très bien dans ce cas. L'original shared_ptrde l'appelant est garanti pour durer plus longtemps que l'appel de fonction, donc la fonction est sûre d'utiliser le shared_ptr<> const &. S'il a besoin de stocker le pointeur pour une date ultérieure, il devra copier (à ce stade, une copie doit être faite, plutôt que de contenir une référence), mais il n'est pas nécessaire d'engager le coût de la copie sauf si vous devez le faire il.
J'irais
3
@David: " Passer une référence constante est bien dans ce cas. " Pas dans une API sensée - pourquoi une API exigerait-elle un type de pointeur intelligent dont il ne profite même pas plutôt que de simplement prendre une référence const normale? C'est presque aussi grave que le manque de const-exactitude. Si vous dites que techniquement, cela ne nuit à rien, alors je suis certainement d'accord, mais je ne pense pas que ce soit la bonne chose à faire.
ildjarn
2
... si d'un autre côté, la fonction n'a pas besoin d'étendre la durée de vie de l'objet, alors vous pouvez simplement supprimer le shared_ptrde l'interface et passer une constréférence plain ( ) à l'objet pointé.
David Rodríguez - dribeas
3
@David: Que faire si votre consommateur d'API n'utilise pas shared_ptr<>au départ? Maintenant, votre API n'est vraiment qu'une douleur dans le cou, car elle oblige l'appelant à changer inutilement la sémantique de la durée de vie de l'objet juste pour l'utiliser. Je ne vois rien qui vaille la peine d'être préconisé à cet égard, à part de dire "techniquement, cela ne nuit à rien". Je ne vois rien de controversé à propos de «si vous n'avez pas besoin de propriété partagée, ne l'utilisez pas shared_ptr<>».
ildjarn
0

dans votre fonction, DoSomething vous modifiez un membre de données d'une instance de classe de myClass sorte que ce que vous modifiez est l'objet géré (pointeur brut) et non l'objet (shared_ptr). Ce qui signifie qu'au point de retour de cette fonction, tous les pointeurs partagés vers le pointeur brut géré verront leur membre de données: myClass::someFieldchangé en une valeur différente.

dans ce cas, vous passez un objet à une fonction avec la garantie que vous ne le modifiez pas (en parlant de shared_ptr et non de l'objet possédé).

L'idiome pour exprimer cela est à travers: un const ref, comme ça

void DoSomething(const std::shared_ptr<myClass>& arg)

De même, vous assurez à l'utilisateur de votre fonction que vous n'ajoutez pas un autre propriétaire à la liste des propriétaires du pointeur brut. Cependant, vous avez conservé la possibilité de modifier l'objet sous-jacent pointé par le pointeur brut.

CAVEAT: Ce qui signifie que si, d'une manière ou d'une autre, quelqu'un appelle shared_ptr::resetavant d'appeler votre fonction, et à ce moment-là, c'est le dernier shared_ptr possédant le raw_ptr, alors votre objet sera détruit et votre fonction manipulera un pointeur pendant pour détruire l'objet. TRÈS DANGEREUX!!!

homme organique
la source