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

94

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

Quels sont les avantages et inconvénients possibles du renvoi par référence ou par valeur?

Deux indices possibles:

  • Destruction précoce des objets. Si je retourne la shared_ptrréférence by (const), le compteur de référence n'est pas incrémenté, donc j'encours le risque de voir l'objet supprimé lorsqu'il sort de la portée dans un autre contexte (par exemple un autre thread). Est-ce correct? Et si l'environnement est monothread, cette situation peut-elle également se produire?
  • Coût. Le passage par valeur n'est certainement pas gratuit. Vaut-il la peine de l'éviter autant que possible?

Merci à tous.

Vincenzo Pii
la source

Réponses:

114

Renvoie les pointeurs intelligents par valeur.

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

Le problème de coût est de nos jours sans objet grâce à l' optimisation de la valeur de retour (RVO), vous n'encourrez donc pas une séquence d'incrémentation-incrémentation-décrémentation ou quelque chose du genre dans les compilateurs modernes. Donc, la meilleure façon de renvoyer a shared_ptrest simplement de retourner par valeur:

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

C'est une opportunité RVO évidente 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 de déplacement de C ++ 11, cette préoccupation est encore moins pertinente. (Mais le seul moyen d'être sûr est de profiler et d'expérimenter.)

Si vous n'êtes toujours pas convaincu, Dave Abrahams a un article qui propose un retour par valeur. Je reproduis un extrait ici; Je vous recommande vivement d'aller lire l'article en entier:

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 savoir mieux, cela me rend nerveux. En principe, lors des get_names() retours, nous devons copier a vectorde l' stringart. Ensuite, nous devons le copier à nouveau lors de l'initialisation names, et nous devons détruire la première copie. S'il y a N strings dans le vecteur, chaque copie peut nécessiter jusqu'à N + 1 allocations de mémoire et toute une série d'accès aux données non compatibles avec le cache> lorsque le contenu de la chaîne est copié.

Plutôt que d'affronter ce genre d'anxiété, je me suis souvent rabattu sur le passage par 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 const-ness parce que nous mutons des noms.
  • Comme les programmeurs fonctionnels aiment nous le rappeler, 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 manière pour gagner en efficacité? Heureusement, la réponse s'avère non (et surtout pas si vous utilisez C ++ 0x).

In silico
la source
Je ne sais pas si je dirais que RVO rend la question sans objet puisque le retour par référence rend décidément RVO impossible.
Edward Strange
@CrazyEddie: C'est vrai, c'est l'une des raisons pour lesquelles je recommande que l'OP retourne par valeur.
In silico
La règle RVO, autorisée par la norme, l'emporte-t-elle sur les règles sur les relations de synchronisation / arrive avant, garanties par la norme?
edA-qa mort-ora-y
1
@ edA-qa mort-ora-y: RVO est explicitement autorisé même s'il a des effets secondaires. Par exemple, si vous avez une cout << "Hello World!";instruction dans un constructeur par défaut et de copie, vous ne verrez pas deux Hello World!s lorsque RVO est en vigueur. Cependant, cela ne devrait pas être un problème pour les pointeurs intelligents correctement conçus, même pour la synchronisation.
In silico
23

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

Deuxièmement, regardez l'implémentation 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 suit les utilisations du pointeur va vous ralentir plus que la simple construction de l'objet shared_ptr.

Edit, 3 ans plus tard: avec l'avènement des fonctionnalités plus modernes de C ++, je modifierais ma réponse pour être plus acceptant les cas où vous avez simplement écrit un lambda qui ne vit jamais en dehors de la portée de la fonction appelante, et qui n'est pas copié ailleurs. Ici, si vous vouliez économiser la surcharge très minimale de 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.

San Jacinto
la source