web-dev-qa-db-fra.com

Comment retourner des pointeurs intelligents (shared_ptr), par référence ou par valeur?

Disons que j'ai une classe avec une méthode qui retourne un shared_ptr.

Quels sont les avantages et inconvénients possibles de le renvoyer par référence ou par valeur?

Deux indices possibles:

  • Destruction d'objet précoce. Si je retourne le shared_ptr par référence (const), le compteur de références n’est pas incrémenté, ainsi j’expose le risque de suppression de l’objet lorsqu’il sort de la portée dans un autre contexte (par exemple un autre thread). Est-ce correct? Et si l’environnement est mono-thread, cette situation peut-elle aussi se produire?
  • Coût. La valeur au passage n'est certainement pas gratuite. Vaut-il la peine de l'éviter autant que possible?

Merci à tous.

81
Vincenzo Pii

Renvoie les pointeurs intelligents par valeur.

Comme vous l'avez dit, si vous le renvoyez par référence, vous n'augmenterez pas correctement le nombre de références, ce qui présente le risque de supprimer quelque chose au moment opportun. Cela seul devrait être une raison suffisante pour ne pas revenir par référence. Les interfaces doivent être robustes.

De nos jours, le problème des coûts est sans objet grâce à optimisation de la valeur de retour (RVO), de sorte que vous ne subirez pas de séquence incrément-incrémentation-décrément ou quelque chose de similaire dans les compilateurs modernes. Donc, le meilleur moyen de retourner un shared_ptr Est simplement de retourner par valeur:

shared_ptr<T> Foo()
{
    return shared_ptr<T>(/* acquire something */);
};

Ceci est une opportunité évidente pour RVO pour les compilateurs C++ modernes. Je sais pertinemment que les compilateurs Visual C++ implémentent RVO même lorsque toutes les optimisations sont désactivées. Et avec la sémantique des mouvements de C++ 11, cette préoccupation est encore moins pertinente. (Mais la seule façon d’en être sûr est de profiler et d’expérimenter.)

Si vous n'êtes toujours pas convaincu, Dave Abrahams a n article , ce qui justifie le retour par valeur. Je reproduis un extrait ici; Je vous recommande vivement de lire l'intégralité de l'article:

Soyez honnête: que ressentez-vous avec le code suivant?

std::vector<std::string> get_names();
...
std::vector<std::string> const names = get_names();

Franchement, même si je devrais le savoir mieux, cela me rend nerveux. En principe, lorsque get_names() retourne, nous devons copier un vector sur strings. Ensuite, nous devons le copier à nouveau lorsque nous initialisons names, et nous devons détruire la première copie. S'il y a N strings dans le vecteur, chaque copie peut nécessiter autant d'allocations de mémoire que N + 1 et toute une série d'accès aux données anti-cache> lorsque le contenu de la chaîne est copié.

Plutôt que de faire face à ce genre d’inquiétude, j’ai souvent eu recours à la procédure de référence pour éviter les copies inutiles:

get_names(std::vector<std::string>& out_param );
...
std::vector<std::string> names;
get_names( names );

Malheureusement, cette approche est loin d'être idéale.

  • Le code a augmenté de 150%
  • Nous avons dû abandonner constness car nous modifions des noms.
  • Comme le rappellent les programmeurs fonctionnels, la mutation rend le code plus complexe à raisonner en sapant la transparence référentielle et le raisonnement équationnel.
  • Nous n'avons plus de sémantique de valeur stricte pour les noms.

Mais est-il vraiment nécessaire de gâcher notre code de cette façon pour gagner en efficacité? Heureusement, la réponse s'avère non (et surtout pas si vous utilisez C++ 0x).

99
In silico

En ce qui concerne n'importe lequel pointeur intelligent (pas seulement shared_ptr), je ne pense pas qu'il soit jamais acceptable de renvoyer une référence à une référence, et j'hésiterais beaucoup à les transmettre par référence ou par un pointeur brut. Pourquoi? Parce que vous ne pouvez pas être sûr qu'il ne sera pas copié superficiellement via une référence ultérieurement. Votre premier point définit la raison pour laquelle cela devrait être une préoccupation. Cela peut se produire même dans un environnement mono-thread. Vous n'avez pas besoin d'un accès simultané aux données pour mettre une mauvaise sémantique de copie dans vos programmes. Vous ne contrôlez pas vraiment ce que vos utilisateurs font avec le pointeur une fois que vous l'avez passé, donc n'encouragez pas l'utilisation abusive en donnant à vos utilisateurs d'API suffisamment de corde pour se pendre.

Deuxièmement, examinez la mise en œuvre de votre pointeur intelligent, si possible. La construction et la destruction devraient être presque négligeables. Si cette surcharge n'est pas acceptable, n'utilisez pas de pointeur intelligent! Mais au-delà de cela, vous devrez également examiner l'architecture de concurrence que vous avez, car un accès mutuellement exclusif au mécanisme qui surveille les utilisations du pointeur va vous ralentir davantage que la simple construction de l'objet shared_ptr.

Edit, 3 ans plus tard: avec l'avènement des fonctionnalités plus modernes en C++, je modifierais ma réponse pour mieux accepter les cas où vous avez simplement écrit un lambda qui ne vit jamais en dehors de la portée de la fonction d'appel et qui n'est pas copié ailleurs. Ici, si vous vouliez éviter la surcharge minimale liée à la copie d'un pointeur partagé, ce serait juste et sûr. Pourquoi? Parce que vous pouvez garantir que la référence ne sera jamais mal utilisée.

19
San Jacinto