Faut-il passer un shared_ptr par référence ou par valeur?

270

Quand une fonction prend un shared_ptr(de boost ou C ++ 11 STL), le passez-vous:

  • par référence const: void foo(const shared_ptr<T>& p)

  • ou par valeur void foo(shared_ptr<T> p):?

Je préférerais la première méthode car je pense qu'elle serait plus rapide. Mais cela en vaut-il vraiment la peine ou y a-t-il des problèmes supplémentaires?

Pourriez-vous s'il vous plaît donner les raisons de votre choix ou le cas échéant, pourquoi vous pensez que cela n'a pas d'importance.

Danvil
la source
14
Le problème est que ceux-ci ne sont pas équivalents. La version de référence crie «Je vais en alias certains shared_ptr, et je peux le changer si je veux.», Tandis que la version de valeur dit «Je vais copier votre shared_ptr, donc pendant que je peux le changer, vous ne le saurez jamais. ) Un paramètre const-reference est la vraie solution, qui dit "Je vais en alias certains shared_ptr, et je promets de ne pas le changer." (Ce qui est extrêmement similaire à la sémantique par valeur!)
GManNickG
2
Hé, je serais intéressé par votre opinion sur le retour d' un shared_ptrmembre de la classe. Le faites-vous par const-refs?
Johannes Schaub - litb
La troisième possibilité est d'utiliser std :: move () avec C ++ 0x, ceci permute les deux shared_ptr
Tomaka17
@Johannes: Je le retournerais par const-reference juste pour éviter toute copie / comptage de références. Là encore, je renvoie tous les membres par const-reference, sauf s'ils sont primitifs.
GManNickG
doublon possible de C ++ - pointeur passant la question
kennytm

Réponses:

229

Cette question a été discutée et répondue par Scott, Andrei et Herb lors de la session Ask Us Anything à C ++ and Beyond 2011 . Regardez à partir de 4:34 sur les shared_ptrperformances et l'exactitude .

En bref, il n'y a aucune raison de passer par la valeur, sauf si le but est de partager la propriété d'un objet (par exemple entre différentes structures de données ou entre différents threads).

À moins que vous ne puissiez le déplacer-l'optimiser comme expliqué par Scott Meyers dans la vidéo de discussion liée ci-dessus, mais cela est lié à la version réelle de C ++ que vous pouvez utiliser.

Une mise à jour majeure de cette discussion a eu lieu lors du panel interactif de la conférence GoingNative 2012 : Ask Us Anything! ce qui vaut le détour, surtout à partir de 22h50 .

mloskot
la source
5
mais comme indiqué ici, il est moins cher de passer par valeur: stackoverflow.com/a/12002668/128384 ne devrait pas être pris en compte également (au moins pour les arguments du constructeur, etc. où un shared_ptr va devenir membre de la classe)?
stijn
2
@stijn Oui et non. Les questions et réponses que vous pointez sont incomplètes, à moins qu'elles ne clarifient la version de la norme C ++ à laquelle elles se réfèrent. Il est très facile de diffuser des règles générales jamais / toujours qui sont tout simplement trompeuses. À moins que les lecteurs prennent le temps de se familiariser avec l'article et les références de David Abrahams, ou de prendre en compte la date de publication par rapport à la norme C ++ actuelle. Donc, les deux réponses, la mienne et celle que vous avez pointée, sont correctes compte tenu du temps de publication.
mloskot
1
" sauf s'il y a du multi-threading " non, MT n'est en rien spécial.
curiousguy
3
Je suis super en retard à la fête, mais ma raison de vouloir passer shared_ptr par valeur est que cela rend le code plus court et plus joli. Sérieusement. Value*est court et lisible, mais c'est mauvais, alors maintenant mon code est plein const shared_ptr<Value>&et il est nettement moins lisible et juste ... moins rangé. Ce qui était auparavant void Function(Value* v1, Value* v2, Value* v3)est maintenant void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3), et les gens sont d'accord avec ça?
Alex
7
@Alex La pratique courante consiste à créer des alias (typedefs) juste après la classe. Pour votre exemple: class Value {...}; using ValuePtr = std::shared_ptr<Value>;Alors votre fonction devient plus simple: void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)et vous obtenez des performances maximales. C'est pourquoi vous utilisez C ++, n'est-ce pas? :)
4LegsDrivenCat
92

Voici le point de vue d' Herb Sutter

Ligne directrice: ne passez 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é.

Ligne directrice: indiquez qu'une fonction stockera et partagera la propriété d'un objet de segment de mémoire à l'aide d'un paramètre shared_ptr par valeur.

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

acel
la source
3
Merci pour le lien vers Sutter. C'est un excellent article. Je ne suis pas d'accord avec lui sur le widget *, préférant le <widget &> facultatif si C ++ 14 est disponible. widget * est trop ambigu par rapport à l'ancien code.
Éponyme
3
+1 pour inclure le widget * et le widget & comme possibilités. Juste pour élaborer, passer widget * ou widget & est probablement la meilleure option lorsque la fonction n'examine / modifie pas l'objet pointeur lui-même. L'interface est plus générale, car elle ne nécessite pas de type de pointeur spécifique et le problème de performances du nombre de références shared_ptr est esquivé.
tgnottingham
4
Je pense que cela devrait être la réponse acceptée aujourd'hui, en raison de la deuxième directive. Il invalide clairement la réponse acceptée actuelle, qui dit: il n'y a aucune raison de passer par la valeur.
mbrt
62

Personnellement, j'utiliserais une constréférence. Il n'est pas nécessaire d'incrémenter le nombre de références juste pour le décrémenter à nouveau pour un appel de fonction.

Evan Teran
la source
1
Je n'ai pas voté contre votre réponse, mais avant que ce soit une question de préférence, il y a des avantages et des inconvénients à chacune des deux possibilités à considérer. Et il serait bon de connaître et de discuter de ces avantages et inconvénients. Ensuite, chacun peut prendre une décision pour lui-même.
Danvil
@Danvil: compte tenu du shared_ptrfonctionnement, le seul inconvénient possible à ne pas passer par référence est une légère perte de performances. Il y a deux causes ici. a) la fonction d'alias de pointeur signifie que des pointeurs d'une valeur de données plus un compteur (peut-être 2 pour les références faibles) sont copiés, il est donc légèrement plus cher de copier le tour de données. b) le comptage de référence atomique est légèrement plus lent que l'ancien code d'incrémentation / décrémentation ordinaire, mais il est nécessaire pour garantir la sécurité des threads. Au-delà de cela, les deux méthodes sont les mêmes pour la plupart des intentions et des fins.
Evan Teran
37

Passez par constréférence, c'est plus rapide. Si vous avez besoin de le stocker, disons dans un conteneur, la réf. le compte sera automatiquement incrémenté par l'opération de copie.

Nikolai Fetissov
la source
4
Downvote a exprimé son opinion sans aucun chiffre à l'appui.
kwesolowski
22

J'ai exécuté le code ci-dessous, une fois en fooprenant le shared_ptrby const&et encore une fois en fooprenant la shared_ptrvaleur by.

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

Utilisation de VS2015, version x86, sur mon processeur Intel Core 2 Quad (2,4 GHz)

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

La version de copie par valeur était un ordre de grandeur plus lent.
Si vous appelez une fonction de manière synchrone à partir du thread actuel, préférez la const&version.

tcb
la source
1
Pouvez-vous indiquer les paramètres du compilateur, de la plate-forme et de l'optimisation que vous avez utilisés?
Carlton
J'ai utilisé la version de débogage de vs2015, mis à jour la réponse pour utiliser la version de version maintenant.
tcb
1
Je suis curieux de savoir si lorsque l'optimisation est activée, vous obtenez les mêmes résultats avec les deux
Elliot Woods
2
L'optimisation n'aide pas beaucoup. le problème est la contention du verrou sur le nombre de références sur la copie.
Alex
1
Ce n'est pas le propos. Une telle foo()fonction ne devrait même pas accepter un pointeur partagé en premier lieu car elle n'utilise pas cet objet: elle doit accepter a int&et do p = ++x;, appelant foo(*p);depuis main(). Une fonction accepte un objet pointeur intelligent lorsqu'elle doit en faire quelque chose, et la plupart du temps, ce que vous devez faire, c'est le déplacer ( std::move()) ailleurs, donc un paramètre de valeur n'a aucun coût.
eepp
15

Depuis C ++ 11, vous devriez le prendre par valeur sur const & plus souvent que vous ne le pensez.

Si vous prenez le std :: shared_ptr (plutôt que le type sous-jacent T), vous le faites parce que vous voulez en faire quelque chose.

Si vous souhaitez le copier quelque part, il est plus logique de le prendre par copie, et std :: le déplacer en interne, plutôt que de le prendre par const & puis de le copier plus tard. Cela est dû au fait que vous autorisez l'appelant à tour à tour std :: déplacer le shared_ptr lors de l'appel de votre fonction, vous économisant ainsi un ensemble d'opérations d'incrémentation et de décrémentation. Ou pas. C'est-à-dire que l'appelant de la fonction peut décider s'il a besoin ou non du std :: shared_ptr après avoir appelé la fonction, et selon qu'il se déplace ou non. Ce n'est pas réalisable si vous passez par const &, et c'est donc de préférence de le prendre en valeur.

Bien sûr, si l'appelant a besoin de son shared_ptr plus longtemps (donc ne peut pas std :: le déplacer) et que vous ne voulez pas créer une copie simple dans la fonction (par exemple, vous voulez un pointeur faible, ou vous ne voulez que parfois pour le copier, selon certaines conditions), alors un const & peut être préférable.

Par exemple, vous devez faire

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

plus de

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

Parce que dans ce cas, vous créez toujours une copie en interne

Biscuit
la source
1

Ne sachant pas le coût en temps de l'opération de copie shared_copy où l'incrémentation et la décrémentation atomiques sont effectuées, j'ai souffert d'un problème d'utilisation du processeur beaucoup plus élevé. Je ne m'attendais jamais à ce que l'incrémentation et la décrémentation atomiques coûtent autant.

Suite à mon résultat de test, l'incrémentation et la décrémentation atomiques int32 prennent 2 ou 40 fois l'incrémentation et la décrémentation non atomiques. Je l'ai eu sur 3GHz Core i7 avec Windows 8.1. Le premier résultat sort quand aucune contention ne se produit, le second quand une forte possibilité de contention se produit. Je garde à l'esprit que les opérations atomiques sont enfin des verrous basés sur le matériel. La serrure est la serrure. Mauvais pour les performances en cas de conflit.

Dans ce cas, j'utilise toujours byref (const shared_ptr &) que byval (shared_ptr).

Hyunjik Bae
la source
1

Il y a eu un récent article de blog: https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

La réponse est donc la suivante: ne passez (presque) jamais const shared_ptr<T>&.
Passez simplement la classe sous-jacente à la place.

Fondamentalement, les seuls types de paramètres raisonnables sont:

  • shared_ptr<T> - Modifier et s'approprier
  • shared_ptr<const T> - Ne modifiez pas, prenez possession
  • T& - Modifier, pas de propriété
  • const T& - Ne modifiez pas, pas de propriété
  • T - Ne pas modifier, pas de propriété, pas cher à copier

Comme @accel l'a souligné dans https://stackoverflow.com/a/26197326/1930508, les conseils de Herb Sutter sont les suivants:

N'utilisez un const shared_ptr & comme paramètre que si vous n'êtes pas sûr de prendre ou non une copie et de partager la propriété

Mais dans combien de cas n'êtes-vous pas sûr? C'est donc une situation rare

Flamefire
la source
0

Il est connu que le passage de shared_ptr par valeur a un coût et doit être évité si possible.

Le coût du passage par shared_ptr

La plupart du temps, passer shared_ptr par référence, et encore mieux par référence const, ferait l'affaire.

La directive de base cpp a une règle spécifique pour passer shared_ptr

R.34: Prendre un paramètre shared_ptr pour exprimer qu'une fonction est propriétaire de la partie

void share(shared_ptr<widget>);            // share -- "will" retain refcount

Un exemple de passage de shared_ptr par valeur est vraiment nécessaire lorsque l'appelant transmet un objet partagé à un appelant asynchrone - c'est-à-dire que l'appelant sort de la portée avant que l'appelé termine son travail. L'appelé doit "étendre" la durée de vie de l'objet partagé en prenant un share_ptr par valeur. Dans ce cas, passer une référence à shared_ptr ne fera pas l'affaire.

Il en va de même pour passer un objet partagé à un thread de travail.

artm
la source
-4

shared_ptr n'est pas assez grand, et son constructeur \ destructeur ne fait pas assez de travail pour qu'il y ait suffisamment de surcharge de la copie pour se soucier des performances de passage par référence vs de passage par copie.

stonemetal
la source
15
L'avez-vous mesuré?
curiousguy
2
@stonemetal: Qu'en est-il des instructions atomiques lors de la création d'un nouveau shared_ptr?
Quarra
Il s'agit d'un type non-POD, donc dans la plupart des ABI, même le passer "par valeur" passe en fait un pointeur. Ce n'est pas du tout la copie d'octets qui pose problème. Comme vous pouvez le voir dans la sortie asm, le passage d'une shared_ptr<int>valeur by prend plus de 100 instructions x86 (y compris les lockinstructions ed coûteuses pour augmenter / diminuer atomiquement le nombre de références). Passer par ref constante revient à passer un pointeur vers n'importe quoi (et dans cet exemple sur l'explorateur du compilateur Godbolt, l'optimisation de l'appel de queue transforme cela en un jmp simple au lieu d'un appel: godbolt.org/g/TazMBU ).
Peter Cordes
TL: DR: Il s'agit de C ++ où les constructeurs de copie peuvent faire beaucoup plus de travail que de simplement copier les octets. Cette réponse est une ordure totale.
Peter Cordes
2
stackoverflow.com/questions/3628081/shared-ptr-horrible-speed Comme un exemple de pointeurs partagés passés par valeur vs passer par référence, il voit une différence de temps d'exécution d'environ 33%. Si vous travaillez sur du code critique pour les performances, les pointeurs nus vous offrent une augmentation de performances plus importante. Donc, passez par const ref si vous vous en souvenez, mais ce n'est pas grave si vous ne le faites pas. Il est beaucoup plus important de ne pas utiliser shared_ptr si vous n'en avez pas besoin.
stonemetal