Question

Si une fonction doit 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 boost::shared_ptr<foo>)? Quels sont les effets secondaires possibles? J'envisage deux cas possibles:

1) une copie de l'argument est créée dans la fonction, 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();  
    ...  
}  

Je ne vois dans les deux cas aucune bonne raison de passer le <=> par valeur plutôt que par référence. Passer par valeur ne ferait que & Quot; temporairement & Quot; incrémentez le nombre de références en raison de la copie, puis décrémentez-le lorsque vous quittez 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.

Était-ce utile?

La solution

L’intérêt d’une instance shared_ptr distincte est de garantir (dans la mesure du possible) que, tant que cet élément sp->do_something() est dans la portée, l’objet pointé existera toujours car son nombre de références sera au moins égal à 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 std::string, 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 Hi! 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 send_message sur le même objet? Et que se passe-t-il s’il s’agit du seul élément distinctif msg restant à cet objet? Adieu objet, juste à l'endroit où vous allez essayer de l'utiliser.

Il y a donc 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 soit 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 programmé votre produit dans une situation réaliste dans un profileur et que vous ayez mesuré de manière concluante les modifications que vous souhaitez apporter. fera une différence significative en termes de performances.

Mise à jour du 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. Ainsi, plutôt que d’utiliser un shared_ptr & qui est probablement copié lorsqu’il est transmis à plusieurs endroits, nous utilisons un std::shared_ptr<std::string> à une chaîne:

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

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

Nous souhaitons maintenant ajouter une fonction 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 en fonction des 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 à envoyer, nous ignorons le message précédent, 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 <=> à deux reprises.

Maintenant vient M. Maintainer, qui regarde le code et pense: "Hé, ce paramètre sur <=> est un <=>:

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

Évidemment, cela peut être changé en:

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, de sorte que l'amélioration des performances sera si petite qu'elle ne sera plus mesurable).

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 s’étonne de cela, mais ajoute une vérification défensive à <=> pour tenter d’arrêter le problème:

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

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

Comme je le dis, 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 tiennent pointeurs entre eux, il est facile de faire l'erreur, et difficile de construire les tests élémentaires nécessaires pour détecter l'erreur.

La solution simple, dans laquelle vous voulez qu'une fonction puisse compter sur un <=> non-nullité, consiste à ce que la fonction attribue son propre vrai <=>, plutôt que de s'appuyer sur un existant <=>.

L’inconvénient est que copier un <=> n’est pas gratuit: même & "sans verrouillage " les implémentations 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 <=> en un <=>. 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 <=> dans l'ensemble au lieu de <=>, 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 <=>, le compromis peut donc être différent.

Autres conseils

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. De http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything

Herb Sutter: & "quand vous passez à shared_ptr, les copies coûtent cher &";

Scott Meyers: & "Il n'y a rien de spécial à propos de shared_ptr lorsqu'il s'agit 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 que shared_ptr résout en quelque sorte tous les problèmes de gestion et que, du fait de sa petite taille, il est nécessairement peu coûteux de passer par la valeur. Cela doit être copié, et il y a un coût associé à ça ... ça coûte cher de le passer en valeur, donc si je peux m'en passer avec une sémantique correcte dans mon programme, je vais le passer par référence à const ou référence à 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, alors vous pourriez peut-être passer par valeur ... vous les copiez en tant que paramètres, oh mon Dieu, vous n’avez presque jamais besoin de modifier ce décompte de références, car 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 passer le mot shared_ptr du tout "sauf si vous souhaitez utiliser ou manipuler le pointeur intelligent lui-même, par exemple pour partager ou transférer la propriété. &";

Je déconseillerais cette pratique à moins que vous et les autres programmeurs avec qui vous travaillez ne vraiment 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. Commencez par 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 valeur), mais plutôt de vous soucier de la conception, des structures de données, des algorithmes et de la maintenabilité à long terme.

Oui, prendre une référence va bien ici. Vous n'avez pas l'intention de donner à la méthode une propriété partagée; il veut seulement travailler avec cela. Vous pouvez également prendre une référence pour le premier cas, puisque vous la copiez quand même. Mais dans le premier cas, il en prend possession . 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 retournez (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 produirait). S'il s'avère que c'est un goulot d'étranglement (premier profil!), Vous pouvez toujours renvoyer une référence.

Modifier : bien entendu, comme le soulignent d'autres personnes, 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, indiquez une valeur.

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

Essayez d'utiliser <=> plutôt que seulement <=>, 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)

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);

Je voudrais éviter un " plain " référence à moins que la fonction puisse modifier explicitement le pointeur.

Un const & peut être une micro-optimisation judicieuse lorsque vous appelez 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 cependant pas à ce que cela fasse une grande différence dans la plupart des scénarios.

En règle générale, vous devez utiliser le style plus simple, sauf si vous avez des raisons de ne pas le faire. Ensuite, utilisez la <=> cohérence, ou ajoutez un commentaire expliquant pourquoi, si vous ne l'utilisez que dans quelques endroits.

Je recommanderais de passer un pointeur partagé par une référence const - une sémantique selon laquelle la fonction transmise 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ûre 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 invoquer à l'intérieur de manière à détruire l'objet qui est toujours considéré comme valide après le retour de la fonction.

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

Seulement mes 2 centimes: -)

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 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 en raison d'un pointeur en suspens?

La vérité est que, shared_ptr ou non, const ou non, cela pourrait se produire si vous rencontrez un problème de conception, par exemple si vous partagez directement ou indirectement la propriété de sp entre les threads, en utilisant mal un objet qui delete this, vous avoir une erreur de propriété circulaire ou d’autres erreurs de propriété.

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;
}

Je suppose que vous connaissez bien optimisation prématurée . et vous le demandez soit à des fins académiques, soit parce que vous avez isolé un code préexistant qui est peu performant.

Passer par référence est acceptable

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

Vous n'ê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 des références est souvent plus efficace pour les raisons que vous avez mentionnées, mais non garanties . 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, dans lesquelles le pointeur serait passé d'une fonction à une autre avant d'être utilisé.

Vous devez toujours éviter de stocker votre pointeur intelligent en tant que référence. Votre Class::take_copy_of_sp(&sp) exemple montre l'utilisation correcte pour cela.

En supposant que la constance ne nous concerne pas (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 par référence car 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 passer sharedA par référence permet à FooTakesReference de réinitialiser le pointeur d'origine, ce qui réduit le nombre d'utilisations à 0 et détruit les 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 la fragile existence de sharedA , un chaos s'ensuit. Le chanceux développeur sharedB , cependant, rentre chez lui tôt car tout va bien dans son monde.

La sécurité du code, dans ce cas, dépasse de loin toutes 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.

Sandy a écrit: & "On dirait que tous les avantages et les inconvénients peuvent ici être généralisés à N'IMPORTE QUEL type passé par référence et pas seulement à shared_ptr. &";

Vrai dans une certaine mesure, mais l’intérêt de shared_ptr est d’éliminer les problèmes de durée de vie des objets et de laisser le compilateur le gérer pour vous. Si vous souhaitez passer un pointeur partagé par référence et autoriser les clients de votre méthode de référence à compter les objets appelant une référence qui pourrait libérer les données de l'objet, alors utiliser un pointeur partagé est presque inutile.

J'ai écrit & "presque &"; dans cette phrase précédente, car les performances peuvent être une source de préoccupation et "cela peut être" justifié dans de rares cas, mais j’éviterais moi-même ce scénario et rechercherais moi-même toutes les autres solutions d’optimisation possibles, comme envisager sérieusement d’ajouter un indirection, é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, requiert 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 être comptés. copié rapidement. (Bien entendu, ce n'est qu'un scénario de conception qui pourrait ne pas s'appliquer à votre situation.)

J'avais l'habitude de travailler dans un projet où le principe était très fort pour passer des indicateurs intelligents par valeur. Lorsqu’on m’a demandé de faire une analyse des performances, j’ai constaté que, pour l’incrémentation et la décrémentation des compteurs de référence des pointeurs intelligents, l’application consacrait entre 4 et 6% du temps processeur utilisé.

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

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 que vous utilisez dans l'interface.

En plus de ce que litb a dit, je voudrais souligner que c'est probablement passer référence par référence dans le deuxième exemple, de cette façon, vous êtes sûr de ne pas le modifier accidentellement.

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;
    }
    

Chaque morceau de code doit avoir un sens. Si vous transmettez un pointeur partagé par une 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 quot ;. Ce n’est pas ce que j’appelle un signe de confiance envers d’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 également être vu comme un indice sur le dessin (& "; Le pointeur pourrait être modifié ailleurs &";).

Donc oui, IMO, la valeur par défaut doit être & "; passer par la référence du const" & ";

.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top