web-dev-qa-db-fra.com

C++ - passer des références à std :: shared_ptr ou boost :: shared_ptr

Si j’ai une fonction devant fonctionner avec un shared_ptr, ne serait-il pas plus efficace de lui transmettre une référence (afin d’éviter de copier l’objet shared_ptr)? Quels sont les éventuels effets secondaires? ?____ .J’envisage deux cas possibles:

1) dans la fonction, l'argument est copié, comme dans

ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)  
{  
     ...  
     m_sp_member=sp; //This will copy the object, incrementing refcount  
     ...  
}  

2) dans la fonction, l'argument n'est utilisé que, comme dans 

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}  

Dans les deux cas, je ne vois pas de bonne raison de passer le boost::shared_ptr<foo> par valeur plutôt que par référence. Passer par valeur incrémenterait seulement "temporairement" le compte de référence en raison de la copie, puis le décrémenterait lors de la sortie de l'étendue de la fonction . Est-ce que je néglige quelque chose?

Juste pour clarifier, après avoir lu plusieurs réponses: je suis parfaitement d’accord sur les préoccupations d’optimisation prématurée et j’essaie toujours de profiler ensuite, puis de travailler sur les points chauds. Ma question portait davantage sur un code purement technique, si vous voyez ce que je veux dire.

113
abigagli

Le but d'une instance distincte shared_ptr est de garantir (dans la mesure du possible) que, tant que ce shared_ptr est dans la portée, l'objet vers lequel il pointe existera toujours car son nombre de références sera d'au moins 1.

Class::only_work_with_sp(boost::shared_ptr<foo> sp)
{
    // sp points to an object that cannot be destroyed during this function
}

Donc, en utilisant une référence à un shared_ptr, vous désactivez cette garantie. Donc dans votre deuxième cas:

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}

Comment savez-vous que sp->do_something() ne va pas exploser à cause d'un pointeur nul?

Tout dépend de ce qu'il y a dans ces sections '...' du code. Que faire si vous appelez quelque chose pendant le premier '...' qui a pour effet secondaire (quelque part dans une autre partie du code) de supprimer un shared_ptr sur le même objet? Et que se passe-t-il s'il s'agit du seul shared_ptr distinct restant pour cet objet? Adieu objet, juste à l'endroit où vous allez l'essayer.

Donc, il y a deux façons de répondre à cette question:

  1. Examinez très attentivement la source de tout votre programme jusqu'à ce que vous soyez sûr que l'objet ne mourra pas pendant le corps de la fonction.

  2. Modifiez le paramètre pour qu'il devienne un objet distinct au lieu d'une référence.

Conseil général qui s’applique ici: ne vous donnez pas la peine d’apporter des modifications risquées à votre code pour des raisons de performances, jusqu’à ce que vous ayez chronométré votre produit dans une situation réaliste dans un profileur et que vous ayez mesuré de manière concluante le fait que les modifications que vous souhaitez apporter apporteront une modification. différence significative à la performance.

Mise à jour pour le commentateur JQ

Voici un exemple artificiel. C'est délibérément simple, l'erreur sera donc évidente. Dans les exemples réels, l'erreur n'est pas si évidente car elle est cachée dans des couches de détails réels.

Nous avons une fonction qui enverra un message quelque part. Il peut s'agir d'un message volumineux. Par conséquent, plutôt que d'utiliser un std::string qui est probablement copié au fur et à mesure de sa transmission, nous utilisons un shared_ptr dans une chaîne:

void send_message(std::shared_ptr<std::string> msg)
{
    std::cout << (*msg.get()) << std::endl;
}

(Nous venons de "l'envoyer" à la console pour cet exemple).

Maintenant, nous voulons ajouter une installation pour mémoriser le message précédent. Nous souhaitons le comportement suivant: il doit exister une variable contenant le dernier message envoyé, mais pendant l'envoi d'un message, il ne doit pas y avoir de message précédent (la variable doit être réinitialisée avant l'envoi). Nous déclarons donc la nouvelle variable:

std::shared_ptr<std::string> previous_message;

Ensuite, nous modifions notre fonction conformément aux règles que nous avons spécifiées:

void send_message(std::shared_ptr<std::string> msg)
{
    previous_message = 0;
    std::cout << *msg << std::endl;
    previous_message = msg;
}

Donc, avant de commencer l’envoi, nous éliminons le message précédent actuel, puis une fois l’envoi terminé, nous pouvons stocker le nouveau message précédent. Tout bon. Voici un code de test:

send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);

Et comme prévu, cela imprime Hi! deux fois.

Maintenant, M. Maintainer, qui examine le code et pense: Hey, ce paramètre pour send_message est un shared_ptr:

void send_message(std::shared_ptr<std::string> msg)

Évidemment, cela peut être changé pour:

void send_message(const std::shared_ptr<std::string> &msg)

Pensez à l'amélioration des performances que cela apportera! (Peu importe le fait que nous sommes sur le point d’envoyer un message généralement volumineux sur un canal donné, l’amélioration des performances sera tellement minime qu’elle sera impossible à mesurer).

Mais le vrai problème est que le code de test présentera désormais un comportement indéfini (dans les versions de débogage Visual C++ 2010, il se bloque).

M. Maintainer est surpris par cela, mais ajoute un contrôle défensif à send_message pour tenter d'empêcher le problème de se produire:

void send_message(const std::shared_ptr<std::string> &msg)
{
    if (msg == 0)
        return;

Mais bien sûr, cela continue et se bloque, car msg n'est jamais nul lorsque send_message est appelé.

Comme je l'ai dit, avec tout le code si rapproché dans un exemple trivial, il est facile de trouver l'erreur. Mais dans les programmes réels, avec des relations plus complexes entre des objets mutables qui détiennent des pointeurs, il est facile de make l'erreur, et il est difficile de construire les tests élémentaires nécessaires pour détecter l'erreur.

La solution simple, dans laquelle vous souhaitez qu'une fonction puisse compter sur un shared_ptr qui reste non nul, consiste à attribuer son propre shared_ptr vrai, plutôt que de s'appuyer sur une référence à un shared_ptr existant.

L'inconvénient est que copier un shared_ptr n'est pas libre: même les implémentations "sans verrouillage" doivent utiliser une opération verrouillée pour respecter les garanties de threading. Il peut donc y avoir des situations où un programme peut être considérablement accéléré en changeant un shared_ptr en un shared_ptr &. Mais ce n’est pas un changement qui peut être apporté en toute sécurité à tous les programmes. Cela change la signification logique du programme.

Notez qu'un bogue similaire se produirait si nous utilisions std::string dans l'ensemble au lieu de std::shared_ptr<std::string> et au lieu de:

previous_message = 0;

pour effacer le message, nous avons dit:

previous_message.clear();

Le symptôme serait alors l'envoi accidentel d'un message vide, au lieu d'un comportement indéfini. Le coût d'une copie supplémentaire d'une très grande chaîne peut être beaucoup plus important que le coût de la copie d'un shared_ptr, de sorte que le compromis peut être différent.

107
Daniel Earwicker

Je me suis trouvé en désaccord avec la réponse la plus votée, alors je suis allé à la recherche d'opinions d'experts et les voici . From http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond -2011-Scott-Andrei-and-Herb-Ask-Us-Anything

Herb Sutter: "lorsque vous passez à shared_ptr, les copies coûtent cher"

Scott Meyers: "shared_ptr n'a pas de particularité de savoir si vous le transmettez par valeur ou par référence. Utilisez exactement la même analyse que vous utilisez pour tout autre type défini par l'utilisateur. Les gens semblent avoir cette perception qui résout en quelque sorte tous les problèmes de gestion, et parce que c’est petit, c’est nécessairement peu coûteux de passer d’une valeur à une autre, il faut le copier, et il ya un coût associé à cela ... c’est cher de le faire passer d’une valeur à l’autre, alors si je peux m'en tirer avec une sémantique appropriée dans mon programme, je vais le passer par référence à const ou à la place "

Herb Sutter: "transmettez-les toujours par référence à const, et très occasionnellement peut-être parce que vous savez comment vous appelez peut modifier la chose dont vous avez reçu une référence, peut-être alors vous pourriez passer par valeur ... si vous les copiez comme paramètres, oh Mon Dieu, vous n’avez presque jamais besoin d’augmenter ce nombre de références parce qu’il est maintenu en vie de toute façon, et vous devriez le transmettre par référence, alors veuillez le faire "

Mise à jour: Herb a développé ce sujet ici: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ , bien que la morale de l'histoire soit que vous ne devriez pas ne passez pas du tout shared_ptr "à moins que vous ne souhaitiez utiliser ou manipuler le pointeur intelligent lui-même, par exemple pour partager ou transférer la propriété."

109
Jon

Je déconseillerais cette pratique à moins que vous et les autres programmeurs avec qui vous travaillez ne sachiez vraiment, vraiment que vous sachiez ce que vous faites tous.

Tout d'abord, vous ne savez pas comment l'interface de votre classe pourrait évoluer et vous voulez empêcher les autres programmeurs de faire de mauvaises choses. Transmettre un shared_ptr par référence n'est pas quelque chose qu'un programmeur devrait s'attendre à voir, car ce n'est pas idiomatique, ce qui facilite son utilisation incorrecte. Programmez de manière défensive: rendez l'interface difficile à utiliser de manière incorrecte. Passer par référence ne fera qu'inviter des problèmes plus tard.

Deuxièmement, n'optimisez pas tant que vous ne saurez pas que cette classe va poser problème. Etudiez d’abord le profil, puis si votre programme a vraiment besoin du coup de pouce donné en passant par référence, alors peut-être. Sinon, ne vous contentez pas des petites choses (c’est-à-dire les N instructions supplémentaires nécessaires pour passer d’une valeur à l’autre), mais plutôt de la conception, des structures de données, des algorithmes et de la maintenabilité à long terme.

22
Mark Kegel

Oui, prendre une référence est bien ici. Vous n'avez pas l'intention de donner à la méthode une propriété partagée; il veut seulement travailler avec ça. Vous pouvez aussi prendre une référence pour le premier cas, puisque vous la copiez quand même. Mais pour le premier cas, il prend la propriété. Il y a cette astuce pour ne le copier qu'une seule fois:

void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) {
    m_sp_member.swap(sp);
}

Vous devez également copier lorsque vous le renvoyez (c.-à-d. Ne pas renvoyer de référence). Parce que votre classe ne sait pas ce que fait le client avec elle (elle pourrait stocker un pointeur sur elle et un big bang se produira). S'il s'avère que c'est un goulot d'étranglement (premier profil!), Vous pouvez toujours renvoyer une référence.


Edit : Bien sûr, comme le soulignent d’autres, cela n’est vrai que si vous connaissez votre code et savez que vous ne réinitialisez pas le pointeur partagé transmis. En cas de doute, il suffit de passer par valeur.

18

Il est judicieux de passer shared_ptrs par const&. Cela ne posera probablement pas de problème (sauf dans le cas peu probable où le shared_ptr référencé est supprimé lors de l'appel de la fonction, comme détaillé par Earwicker) et ce sera probablement plus rapide si vous en passez beaucoup. Rappelles toi; boost::shared_ptr par défaut est thread-safe, sa copie inclut donc un incrément de thread-safe.

Essayez d'utiliser const& plutôt que simplement &, car les objets temporaires ne peuvent pas être transmis par une référence non-const. (Même si une extension de langue dans MSVC vous permet de le faire quand même)

11
Magnus Hoff

Dans le second cas, cela est plus simple:

Class::only_work_with_sp(foo &sp)
{    
    ...  
    sp.do_something();  
    ...  
}

Vous pouvez l'appeler comme

only_work_with_sp(*sp);
10
Greg Rogers

Je recommanderais de passer pointeur partagé par référence const - une sémantique selon laquelle la fonction passée avec le pointeur NE possède PAS le pointeur, ce qui est un idiome pur pour les développeurs.

Le seul piège est dans plusieurs programmes de threads que l'objet pointé par le pointeur partagé est détruit dans un autre thread. Il est donc prudent de dire que l'utilisation de la référence const du pointeur partagé est sécurisée dans un programme à un seul thread.

Passer un pointeur partagé par référence non-const est parfois dangereux - la raison en est les fonctions d'échange et de réinitialisation que la fonction peut appeler à l'intérieur de manière à détruire l'objet toujours considéré comme valide après son retour.

Je suppose qu’il ne s’agit pas d’optimisation prématurée. Il s’agit d’éviter un gaspillage inutile de cycles de traitement lorsque vous savez clairement ce que vous voulez faire et que le codage a été fermement adopté par vos collègues développeurs.

Juste mes 2 centimes :-)

3
zmqiu

Il semble que tous les avantages et les inconvénients peuvent être généralisés à TOUT type passé par référence, pas seulement shared_ptr. À mon avis, vous devez connaître la sémantique de passage par référence, référence constante et valeur et l’utiliser correctement. Mais il n’ya absolument rien de mal à passer shared_ptr par référence, à moins que vous ne pensiez que toutes les références sont mauvaises ...

Pour revenir à l'exemple:

Class::only_work_with_sp( foo &sp ) //Again, no copy here  
{    
    ...  
    sp.do_something();  
    ...  
}

Comment savez-vous que sp.do_something() ne va pas exploser à cause d'un pointeur en suspens?

La vérité est que, shared_ptr ou non, const ou non, cela pourrait arriver si vous aviez un défaut de conception, comme partager directement ou indirectement la propriété de sp entre les threads, en utilisant un objet qui ne delete this pas, vous avez une propriété circulaire les erreurs.

3
Sandy

J'éviterais une référence "simple" à moins que la fonction ne puisse explicitement modifier le pointeur. 

Un const & peut être une micro-optimisation judicieuse lors de l’appel de petites fonctions - par ex. pour permettre d’autres optimisations, comme l’inclusion de certaines conditions. De plus, l'incrément/décrément - puisqu'il est thread-safe - est un point de synchronisation. Je ne m'attendrais pas à ce que cela fasse une grande différence dans la plupart des scénarios.

En règle générale, vous devriez utiliser le style plus simple, sauf si vous avez des raisons de ne pas le faire. Ensuite, utilisez le const & de manière cohérente ou ajoutez un commentaire expliquant pourquoi si vous l’utilisez seulement à quelques endroits.

3
peterchen

Une chose que je n'ai pas encore vue mentionnée est que lorsque vous transmettez des pointeurs partagés par référence, vous perdez la conversion implicite que vous obtenez si vous souhaitez transmettre un pointeur partagé de classe dérivée via une référence à un pointeur partagé de classe de base.

Par exemple, ce code générera une erreur, mais fonctionnera si vous modifiez test() afin que le pointeur partagé ne soit pas transmis par référence.

#include <boost/shared_ptr.hpp>

class Base { };
class Derived: public Base { };

// ONLY instances of Base can be passed by reference.  If you have a shared_ptr
// to a derived type, you have to cast it manually.  If you remove the reference
// and pass the shared_ptr by value, then the cast is implicit so you don't have
// to worry about it.
void test(boost::shared_ptr<Base>& b)
{
    return;
}

int main(void)
{
    boost::shared_ptr<Derived> d(new Derived);
    test(d);

    // If you want the above call to work with references, you will have to manually cast
    // pointers like this, EVERY time you call the function.  Since you are creating a new
    // shared pointer, you lose the benefit of passing by reference.
    boost::shared_ptr<Base> b = boost::dynamic_pointer_cast<Base>(d);
    test(b);

    return 0;
}
2
Malvineous

Je suppose que vous connaissez l'optimisation prématurée et que vous posez cette question à des fins académiques ou parce que vous avez isolé un code préexistant qui est peu performant.

Passer par référence, c'est bon

Passer par la référence const est préférable, et peut généralement être utilisé, car cela ne force pas la const-ness sur l'objet pointé.

Vous êtes pas _ susceptible de perdre le pointeur en raison de l'utilisation d'une référence. Cette référence est la preuve que vous avez une copie du pointeur intelligent plus tôt dans la pile et qu'un seul thread possède une pile d'appels, afin que la copie préexistante ne disparaisse pas.

L'utilisation de références est souvent plus efficace pour les raisons que vous avez mentionnées, mais pas garanti. Rappelez-vous que le déréférencement d'un objet peut également prendre du travail. Votre scénario idéal d'utilisation de référence serait que votre style de codage implique de nombreuses petites fonctions, où le pointeur serait passé d'une fonction à une autre avant d'être utilisé.

Vous devriez évitez toujours de stocker votre pointeur intelligent en tant que référence. Votre exemple Class::take_copy_of_sp(&sp) indique une utilisation correcte pour cela.

1
Drew Dormann

En supposant que nous ne sommes pas concernés par l'exactitude de const (ou plus, vous voulez permettre aux fonctions de modifier ou de partager la propriété des données transmises), il est plus sûr de passer un boost :: shared_ptr par valeur que de le faire par référence nous permettons au boost original :: shared_ptr de contrôler sa propre durée de vie. Considérez les résultats du code suivant ...

void FooTakesReference( boost::shared_ptr< int > & ptr )
{
    ptr.reset(); // We reset, and so does sharedA, memory is deleted.
}

void FooTakesValue( boost::shared_ptr< int > ptr )
{
    ptr.reset(); // Our temporary is reset, however sharedB hasn't.
}

void main()
{
    boost::shared_ptr< int > sharedA( new int( 13 ) );
    boost::shared_ptr< int > sharedB( new int( 14 ) );

    FooTakesReference( sharedA );

    FooTakesValue( sharedB );
}

Dans l'exemple ci-dessus, nous voyons que le passage de sharedA par référence permet àFooTakesReference de réinitialiser le pointeur d'origine, ce qui réduit son nombre d'utilisations à 0, détruisant ainsi ses données. FooTakesValue , cependant, ne peut pas réinitialiser le pointeur d'origine, garantissant que les données de sharedB - sont toujours utilisables. Quand un autre développeur arrive inévitablement et tente de tirer parti de l'existence fragile de sharedA , le chaos s'ensuit. Le développeur chanceux sharedB , cependant, rentre tôt car tout va bien dans son monde.

La sécurité du code, dans ce cas, l'emporte de loin sur les améliorations apportées à la vitesse de copie. Dans le même temps, boost :: shared_ptr est censé améliorer la sécurité du code. Il sera beaucoup plus facile de passer d’une copie à une référence si quelque chose nécessite ce type d’optimisation de niche.

1
Kit10

J'avais l'habitude de travailler dans un projet où le principe était très fort pour passer des indicateurs intelligents en valeur. Quand on m'a demandé d'analyser les performances, j'ai constaté que l'application passe entre 4 et 6% du temps processeur utilisé pour incrémenter et décrémenter les compteurs de référence des pointeurs intelligents.

Si vous souhaitez passer les indicateurs intelligents par valeur, afin d'éviter tout problème dans les cas étranges décrits par Daniel Earwicker, assurez-vous de bien comprendre le prix que vous payez pour cela.

Si vous décidez d'utiliser une référence, la principale raison d'utiliser la référence const est de permettre la conversion ascendante implicite lorsque vous devez passer un pointeur partagé à un objet de la classe qui hérite de la classe utilisée dans l'interface. 

1
gsf

Sandy a écrit: "Il semble que tous les avantages et les inconvénients peuvent être généralisés à TOUT type passé par référence, pas seulement à shared_ptr."

C'est vrai dans une certaine mesure, mais l'utilisation de shared_ptr consiste à éliminer les problèmes de durée de vie des objets et à laisser le compilateur s'en charger à votre place. Si vous voulez passer un pointeur partagé par référence et autoriser les clients de vos méthodes non const appel de référence-compte-objet-objet qui pourraient libérer les données de l'objet, l'utilisation d'un pointeur partagé est presque inutile.

J'ai écrit "presque" dans cette phrase précédente parce que les performances peuvent être une source de préoccupation et que cela peut "être" justifié dans de rares cas, mais j'éviterais aussi ce scénario moi-même et rechercherais moi-même toutes les autres solutions d'optimisation possibles, comme par exemple en ajoutant un autre niveau d'indirection, d'évaluation paresseuse, etc.

Le code existant après l'auteur, ou même la publication de la mémoire de l'auteur, qui nécessite des hypothèses implicites sur le comportement, en particulier sur la durée de vie des objets, nécessite une documentation claire, concise et lisible. De nombreux clients ne le liront pas quand même! La simplicité prime presque toujours sur l'efficacité, et il y a presque toujours d'autres façons d'être efficace. Si vous avez vraiment besoin de transmettre des valeurs par référence pour éviter une copie profonde par les constructeurs de copie de vos objets comptés par référence (et de l'opérateur égal), vous devriez peut-être envisager des moyens pour que les données copiées en profondeur soient des pointeurs comptés pouvant compter copié rapidement. (Bien sûr, ce n'est qu'un scénario de conception qui pourrait ne pas s'appliquer à votre situation).

1
Bill H

En plus de ce que litb a dit, j'aimerais souligner qu'il est probablement nécessaire de passer référence à const dans le deuxième exemple, de cette façon, vous êtes sûr de ne pas le modifier accidentellement.

0
Leon Timmermans
struct A {
  shared_ptr<Message> msg;
  shared_ptr<Message> * ptr_msg;
}
  1. passer par valeur: 

    void set(shared_ptr<Message> msg) {
      this->msg = msg; /// create a new shared_ptr, reference count will be added;
    } /// out of method, new created shared_ptr will be deleted, of course, reference count also be reduced;
    
  2. passer par référence:

    void set(shared_ptr<Message>& msg) {
     this->msg = msg; /// reference count will be added, because reference is just an alias.
     }
    
  3. passer par le pointeur:

    void set(shared_ptr<Message>* msg) {
      this->ptr_msg = msg; /// reference count will not be added;
    }
    
0
Dylan Chen

Chaque pièce de code doit avoir un sens. Si vous passez un pointeur partagé par valeur partout dans l'application, cela signifie "Je ne suis pas sûr de ce qui se passe ailleurs, donc je suis favorable à la sécurité brute". Ce n'est pas ce que j'appelle un signe de confiance envers les autres programmeurs qui pourraient consulter le code.

Quoi qu'il en soit, même si une fonction obtient une référence const et que vous êtes "incertain", vous pouvez toujours créer une copie du pointeur partagé en tête de la fonction pour ajouter une référence forte au pointeur. Cela pourrait aussi être vu comme un indice sur le dessin ("le pointeur pourrait être modifié ailleurs").

Donc oui, IMO, la valeur par défaut devrait être " passe par la référence const ".

0
Philippe