Journal Le quiz c++ de l'été

Posté par (page perso) . Licence CC by-sa.
Tags : aucun
20
23
août
2018

Comme c'est le mois du C++ sur LinuxFR, je vous propose ce petit quiz.

Voici un petit morceau de programme, avec une hiérarchie de classe A et sa fille B, deux fonctions qui prennent un pointeur à poil ou un pointeur partagé sur A, et des appels sur ces fonctions avec des A et des B.

class A {};
class B : public A {};

void f(A*);
void g(const std::shared_ptr<A> &);

int main()
{
  auto ptr_a = new A;
  auto ptr_b = new B;

  auto shr_a = std::make_shared<A>();
  auto shr_b = std::make_shared<B>();

  f(ptr_a);
  f(ptr_b);
  g(shr_a);
  g(shr_b);

  return 0;
}

La question est, sur ces 4 appels à la fin du main, est-ce que certains sont plus lents que d'autres, et pourquoi ?

Remarque: Évidemment que dans la grande majorité des programmes, les coûts comparés d'un pointeur partagé et d'un pointeur à poil n'ont aucune importance, les problématiques d'architecture et de qualité du code étant largement prioritaires. Mais ce quiz soulève quelques points intéressants pour qui veut regarder sous le capot, et c'est pour cela que je le soumet.

  • # Ça compile pas

    Posté par (page perso) . Évalué à 3. Dernière modification le 23/08/18 à 15:23.

    Je ne fais pas de C++, par contre je sais comment regarder l'output du code assembleur généré, mais ne connaissant pas le C++, je n'arrive pas à compiler ce bout de code correctement, il manque les includes qui vont bien je pense.

    Voici ce qui m'arrive:

     $ g++ truc.cpp -Og  -o truc
    /usr/bin/ld: /tmp/ccmx3foX.o: in function `main':
    truc.cpp:(.text+0xdf): undefined reference to `f(A*)'
    /usr/bin/ld: truc.cpp:(.text+0xf0): undefined reference to `f(A*)'
    /usr/bin/ld: truc.cpp:(.text+0xfa): undefined reference to `g(std::shared_ptr<A> const&)'
    /usr/bin/ld: truc.cpp:(.text+0x12f): undefined reference to `g(std::shared_ptr<A> const&)'
    collect2: error: ld a retourné le statut de sortie 1
    

    Pour info j'ai rajouté en haut du fichier (suggéré par g++ lors de mon premier essai):

    #include <memory>
    
    • [^] # Re: Ça compile pas

      Posté par (page perso) . Évalué à 3.

      Ah mais je viens de comprendre un truc, f et g sont des signatures de fonction mais n'ont pas d'implémentation, forcément ça ne peut pas marcher.

    • [^] # Re: Ça compile pas

      Posté par (page perso) . Évalué à 3.

      Coucou ! Alors en effet, je n'ai pas mis le code complet afin de ne mettre que les morceaux importants. Voici un exemple compilable. Attention cependant, avec certains niveaux d'optimisations, les appels à f et g pourraient se retrouver inlinés. Pour être sûr que ce n'est pas le cas, il faut les définir dans une unité de compilation séparée (un autre .cpp).

      #include <memory>
      
      class A {};
      class B : public A {};
      
      void f(A*)
      {
      }
      
      void g(const std::shared_ptr<A> &)
      {
      }
      
      int main()
      {
        auto ptr_a = new A;
        auto ptr_b = new B;
      
        auto shr_a = std::make_shared<A>();
        auto shr_b = std::make_shared<B>();
      
        f(ptr_a);
        f(ptr_b);
        g(shr_a);
        g(shr_b);
      
        return 0;
      }
      • [^] # Re: Ça compile pas

        Posté par (page perso) . Évalué à 5. Dernière modification le 23/08/18 à 15:36.

        Même au niveau d'optimisation -Og les appels sont éliminés, en désactivant les optimisations, j'ai bien tout. À première vue et sans trop être sûr, je dirais que f(ptr_a), f(ptr_b) sont les plus rapides et g(shr_a) un peu plus lent, et g(shr_b) encore un peu plus lent, mais encore une fois, sans aller plus loin que juste regarder à quoi ressemble l'ASM non optimisé généré.

        EDIT: Attention j'ai modifié ma réponse. Dans tous les cas le code généré est quasi identique, juste un mov est remplacé par un lea entre f(a) et g(a), et un lea est ajouté en plus pour g(b).

        • [^] # Re: Ça compile pas

          Posté par (page perso) . Évalué à 3.

          Je m'auto répond, mais en fait j'avais pas tenu compte du fait que shr_a et shr_b sont déjà boxées, donc l'appel à aux fonctions, en tout cas dans le code ASM, ne montre finalement pas de différence avec les deux appels précédents, mais j'imagine qu'après lors de l'utilisation de la variable shared il y a un unboxing couteux. J'en sais vraiment rien en fait, je suis très nul en c++.

  • # Un temporaire en plus

    Posté par . Évalué à 6.

    Les trois premiers appels se contentent de passer un pointeur (une référence c'est pointeur caché) et sont donc aussi rapides l'un que l'autre. g(shr_b) a besoin de créer un temporaire de type shared_ptr<A> et une copie de shared_ptr est coûteuse à cause de l'état partagé.

    En pratique, si g n'a pas besoin de posséder la ressource, il faut mieux utiliser pointeur nu ou un weak_ptr. Si g a besoin de la posséder, il faut mieux passer le shared_ptr par valeur: il y a une copie au moment de l'appel dans tous les cas, mais on peut le déplacer par le suite si besoin.

    • [^] # Re: Un temporaire en plus

      Posté par (page perso) . Évalué à 1.

      Au vu du code ASM généré, j'aurais tendance à dire que finalement le shared_ptr à l'air d'être une zero-cost-abstraction, je me trompe ? C'est gcc qui compile ça comme ça ?

    • [^] # Re: Un temporaire en plus

      Posté par . Évalué à 3. Dernière modification le 23/08/18 à 16:44.

      Je dirais plutôt :

      si g n'a pas besoin de posséder la variable ET qu'elle peut être nulle, alors un pointeur nu fait l'affaire
      si g n'a pas besoin de posséder la variable ET qu'elle est forcément définie alors un passage par référence est largement préférable

      Ne pas oublier de mettre des const lorsque c'est possible.

      Il ne faut pas décorner les boeufs avant d'avoir semé le vent

      • [^] # Re: Un temporaire en plus

        Posté par . Évalué à 1.

        si g n'a pas besoin de posséder la variable ET qu'elle est forcément définie alors un passage par référence est largement préférable

        Mais une référence de type A dans ce cas, la référence vers un shared_ptr ne garantie pas que le pointeur lui-même est déréférençable.

        • [^] # Re: Un temporaire en plus

          Posté par . Évalué à 2.

          Mais une référence de type A

          Oui évidemment, c'était clair dans mon esprit, mais peut être mal retranscrit. Le passage d'un shared_ptr ne doit se faire que lorsqu'il y a un partage sur la possession d'un pointeur et uniquement dans ce cas, ce qui normalement est assez rare.

          Il ne faut pas décorner les boeufs avant d'avoir semé le vent

  • # arg! du c++ !

    Posté par . Évalué à 5.

    Je suis assez néophyte en C++ (c'est un langage qui m'a fait fuir assez vite): est-ce que c'est une vraiment question sur le langage C++ ou plutôt sur ce que va faire un compilateur C++ ?

    D'ailleurs, pour ce que j'ai lu sur le C++, j'ai l'impression que la séparation entre la spécification du langage est son implémentation n'est pas toujours simple à faire. Est-ce que c'est une impression très fausse ou il y a un peu de ça ?

    (Et du coup, ça répondrait à ma première question)

    • [^] # Re: arg! du c++ !

      Posté par (page perso) . Évalué à 8.

      Il y a du mieux. Autant pendant un temps, les compilateurs étaient vraiment derrière le standard, autant depuis C++11, on a un standard beaucoup plus clair, et des implémentations qui ont vraiment convergé.

      Par exemple, le standard exclut clairement maintenant l'optimisation COW (copie sur écriture) pour std::string, et tous les compilos sont passés à l'optimisation SSO (optimisation de chaînes courtes. Je n'ai pas trouvé d'article Wikipedia, mais j'avais écrit quelque chose sur le sujet).

      La question porte sur le standard, et sauf grosse surprise je m'attends à ce que tous les compilos fassent la même chose (mais je prends tous les benchmarks que vous avez pour confirmer !).

  • # Godbolt

    Posté par . Évalué à 8.

    On peut voir le résultat de la compilation sur godbolt. J'ai mis GCC et Clang, il y a quelques différences. Mais à vue de nez, je ne sais pas s'il y a vraiment beaucoup de différences.

    Ceci dit, je trouve bizarre de passer un shared_ptr par référence. Soit on a besoin d'un partage et dans ce cas là, on passer par valeur. Soit on n'a pas besoin d'un partage et là, on déréférence le pointeur et on envoie une référence directe sur l'objet. Une référence sur un shared_ptr, ça nécessite deux déréférencement pour atteindre l'objet, c'est un de trop.

    • [^] # Re: Godbolt

      Posté par . Évalué à 5.

      Justement imagine que tu passe ton shared_ptr par copie, puis que tu le stock dans ton objet, tu as 2 copie (avec comptage de référence); certes avec les opérateurs de déplacement y a des chances qu'on ait pas la deuxième copie, mais en pre-c++11 (version boost) on a pas cette mécanique.

      Si tu le passes par références pour le stocker tu n'as qu'une seule copie.

      Cependant il faut bien faire attention lorsque l'on joue avec du multithread

      https://stackoverflow.com/questions/3310737/should-we-pass-a-shared-ptr-by-reference-or-by-value

      Il ne faut pas décorner les boeufs avant d'avoir semé le vent

      • [^] # Re: Godbolt

        Posté par . Évalué à 10.

        J’adore le c++, ou le moindre bout de code trivial à des effets papillon et des conséquences délirantes, saupoudré de “si t’as de la chance (ou malchance, c’est selon), le compilo va te sauver (ou te la faire à l’envers) selon les optimizations)”, tout ça pour ce qui est essentiellement des micros optimizations impossible à mesurer pour la majorité des cas d’usages.
        Changez rien, les gars.

        Linuxfr, le portail francais du logiciel libre et du neo nazisme.

        • [^] # Re: Godbolt

          Posté par (page perso) . Évalué à 7.

          Derrière la petite pique trollesque, tu pointes du doigt un vrai problème, qui est cependant partagé par d'autres couples (langage , compilateur) : le langage donne très peu de garantie d'optimisation ou non de certaines constructions, et le développeur en est réduit à faire confiance au compilateur pour apporter les propriétés de performance, abstraction à coût zéro etc. Autrement dit la phrase « C++ donne du code rapide » est bien moins pertinente que « GCC arrive généralement a compiler efficacement un code C++ ». Ce n'est pas du tout une propriété du langage, et du coup il est difficile de raisonner dessus…

          • [^] # Re: Godbolt

            Posté par . Évalué à 1.

            En C, le comportement des compilateurs est bien plus homogène, ces micro optimisations sont explicites.

        • [^] # Re: Godbolt

          Posté par . Évalué à 5.

          Les autres languages également.
          Sauf que tu ne le sais pas. Tente de déterminer à l'avance le temps d'execution d'une routine en Java ..
          Même pour les microprocesseurs c'est la cas: le temp nécessaire à l'exécution d'une même instruction peut être multiplié par 2 à la génération suivante (à fréquence égale).

        • [^] # Re: Godbolt

          Posté par . Évalué à 3.

          Changez rien, les gars.

          Effectivement, il n'y a rien à changer. C++ permet une gestion très fine de la mémoire et des passages de paramètres (par valeur ou par référence, on a le choix, contrairement à quasiment tous les autres langages), et donc il y a des implications à choisir l'un ou l'autre. Mais ces implications sont très loin des «effets papillons». C'est juste histoire de savoir ce qu'on fait et comment ça se passe. Il y a des gens qui ont besoin de ce genre de subtilités pour avoir des performances, c'est bien qu'il existe un langage qui leur permette de le faire, il s'appelle C++.

        • [^] # Re: Godbolt

          Posté par . Évalué à 5. Dernière modification le 24/08/18 à 09:58.

          Ici y a rien de sorcier, dans le cas de la copie du paramètre pour le stocker ensuite, en C++11, on va avoir (copie + déplacement), en pré c++11 on aura 2 copie; dans les deux cas le résultat est strictement identique.

          Le cas du passage de référence et plus taquin mais on aurait exactement le même problème en pointeur nu, ou en référence sans shared_ptr; si la ressource est désallouée par un autre thread on a une référence sur nulle part (ou un pointeur sur nulle part)

          tout ça pour ce qui est essentiellement des micros optimizations impossible à mesurer pour la majorité des cas d’usages.

          c'est ce qui permet de retourner un conteneur de plusieurs dizaines de milliers d'éléments (potentiellement complexe) sans passer par des pointeurs (du point de vue codeur).

          Presque tous les autres langages passent par référence, mais pas toujours; si tu veux qu'on parle des truc marrant de java ou python, justement sur le passage de paramètre

          • Comment changer une String, un int, ou un Float passé en paramètre d'une fonction en java ? Alors que la quasi totalité des objets ça se fait sans se poser de question.
          • Pourquoi ce p* de paramètre par défaut en python change à chaque appel ?

          Toujours de ce qui en découle, hormis quelques objets, il est impossible de filer un objet en paramètre ou retour d'une fonction (ou d'un objet) et d'être certain qu'il ne va pas être modifié; Alors tu as bien les ImmutableList, mais c'est du runtime. La seule solution c'est de passer par des copies… Là où une const& fait parfaitement l'affaire.

          On en arrive à ce genre de truc http://www.javapractices.com/topic/TopicAction.do?Id=15 ; devoir faire des copies inutiles, des constructions d'objets des allocations…

          Le c++ n'est pas parfait, loin de là, il permet de faire des grosses boulettes, mais je ne connais aucun langage permettant facilement le choix entre copie/référence, const/mutable (y a pas que ça mais c'est le fil du thread)

          Il ne faut pas décorner les boeufs avant d'avoir semé le vent

          • [^] # Re: Godbolt

            Posté par (page perso) . Évalué à 10.

            mais je ne connais aucun langage permettant facilement le choix entre copie/référence, const/mutable

            Tu ne fais pas d'effort, il suffit de se documenter sur Ada :)

            procedure test(A : in Integer) -- Passage par copie car tient sur un registre mais A est constant dans le corps
            procedure test(A : in BigObject) -- Passage par référence car trop gros mais toujours constant dans le corps
            procedure test(A : out Integer) -- La valeur initiale n'a pas d'importance mais là, c'est forcément un référence
            procedure test(A : in out Integer) -- Les deux cas précédents
            procedure test(A : in access BigObject) -- Là, on a clairement affaire à un pointeur qu'il faut déréférencer
            procedure test(A : in not null access BigObject) -- pareil mais on sait qu'il ne sera pas nul
            procedure test(A : in access constant BigObject) -- et celui-là, on ne peut pas modifier le pointé

            Je n'ai pas mis d'exemple mais lorsque l'on passe un objet issu d'un type taggué (en gros, un objet qui autorise la POO), celui-ci est forcément passé par référence.

            Pour les quelques curieux, voici les deux pages intéressantes sur le Wikibook, celle sur les passage de paramètres et celle des pointeurs.

            • [^] # Re: Godbolt

              Posté par (page perso) . Évalué à 7. Dernière modification le 24/08/18 à 13:30.

              Tu ne fais pas d'effort, il suffit de se documenter sur Ada :)

              … et Fortran (et probablement plein d’autres langages de cette génération) !

              interface
                subroutine test(A, B, C, D, E)
                  integer, dimension(6), intent(in) ::    A
                  integer, dimension(6), intent(out) ::   B
                  integer, dimension(6), intent(inout) :: C
                  integer, dimension(6), value ::         D
                end subroutine
              end interface

              Bon, ce n’est pas tout à fait comme en C++ car les “intent” déclarent les intentions d’utilisation d’une variable et laissent le compilateur libre de choisir le passage par référence ou par valeur, mais en terme de liberté pour le programmeur et de capacité d’optimisation, ça offre les mêmes possibilités que les “*”, “&” du C++ et leurs homologues précédés de “const”.

              • [^] # Re: Godbolt

                Posté par (page perso) . Évalué à 4.

                Arg, j’ai oublié “E”

                    integer, dimension(6), intent(inout), optional :: E

                pour le cas où l’on souhaite un type “nullable”.

              • [^] # Re: Godbolt

                Posté par (page perso) . Évalué à 3.

                les “intent” déclarent les intentions d’utilisation d’une variable et laissent le compilateur libre de choisir le passage par référence ou par valeur

                Au final, c'est pareil en Ada et c'est ce qui est dit dans le wikibook

                The parameter passing method depends on the type of the parameter. A rule of thumb is that parameters fitting into a register are passed by copy, others are passed by reference. For certain types, there are special rules, for others the parameter passing mode is left to the compiler (which you can assume to do what is most sensible). Tagged types are always passed by reference.
                Explicitly aliased parameters and access parameters specify pass by reference.

                C'est au final au compilateur de choisir sauf si le développeur veut forcer une manière de passer les arguments et dans ce cas, on peut passer par des pointeurs.

                D'ailleurs pour faciliter le masquage de la sémantique des pointeurs, Ada 2012 a introduit l'aspect Implicit_Dereference qui permet de ne pas avoir à utiliser un .all.

                type Accessor (Data: not null access Element) is limited private
                   with Implicit_Dereference => Data;

                Ainsi, une variable de type Accessor construite sur un pointeur d'Element donne directement le membre Data du type en question.

          • [^] # Re: Godbolt

            Posté par (page perso) . Évalué à 2. Dernière modification le 28/08/18 à 11:28.

            Le c++ n'est pas parfait, loin de là, il permet de faire des grosses boulettes, mais je ne connais aucun langage permettant facilement le choix entre copie/référence, const/mutable (y a pas que ça mais c'est le fil du thread)

            Je ne veux pas être le fanboy de Rust de service, mais c'est ce qu'il fait, en encore mieux, puisqu'il déplace les objets par défaut et ne fait une copie que de façon explicite.

        • [^] # Re: Godbolt

          Posté par . Évalué à 3.

          blablabla a […] des conséquences délirantes
          et
          blablabla impossible à mesurer pour la majorité des cas d’usages

          tu n'as pas l'impression que ta remarque est auto contradictoire ?

      • [^] # Re: Godbolt

        Posté par . Évalué à 2.

        Si tu le passes par références pour le stocker tu n'as qu'une seule copie.

        Le lien que tu donnes dit explicitement «Shortly, there is no reason to pass by value, unless the goal is to share ownership of an object», ce qui est exactement ce que j'ai dit plus haut : «Soit on a besoin d'un partage et dans ce cas là, on passer par valeur.»

        Et après, de manière générale, si tu veux stocker un objet, tu as intérêt à le demander par valeur (ça ne concerne pas que les shared_ptr pour le coup) pour éviter des copies justement. Dans le cas du passage par référence, l'objet va être créé puis copié dans la fonction. Dans le cas du passage par valeur, il va y avoir zéro copie, l'objet va être créé puis déplacé directement.

        Dans le lien que tu donnes, il y a d'ailleurs une réponse qui pointent vers un post de Herb Sutter qui est accessoirement le président du comité de normalisation et qui donne une réponse très détaillée pour tous les cas de figure.

        • [^] # Re: Godbolt

          Posté par . Évalué à 4. Dernière modification le 24/08/18 à 09:58.

          Dans le cas du passage par référence, l'objet va être créé puis copié dans la fonction.

          Hum ? En quoi il s'agit d'une référence du coup ?

          Dans le cas du passage par valeur, il va y avoir zéro copie, l'objet va être créé puis déplacé directement.

          Si on a un constructeur adéquate, c'est ça ?

          Ça se passe comment pour une grappe d'objets ? Si mon objet a référence un objet b et que je fais un déplacement de l'objet a, qu'est-ce qui va définir si on fait une copie ou un déplacement de l'objet b ? C'est transparent à l'utilisation, donc ça dépend juste de si b possède un constructeur de déplacement ?

          • [^] # Re: Godbolt

            Posté par . Évalué à 2.

            Ce ne serait pas juste une inversion des termes par erreur ?

            Dans le cas du passage par référence valeur, l'objet va être créé puis copié dans la fonction.

            Dans le cas du passage par valeur référence, il va y avoir zéro copie, l'objet va être créé puis déplacé directement.

            • [^] # Re: Godbolt

              Posté par . Évalué à 6.

              Non, ce n'est pas inversé. Je vais prendre un exemple avec std::string pour que ce soit plus clair. L'hypothèse de départ, c'est qu'on veut conserver la chaîne de caractères passé en paramètre (sinon, pas besoin de se poser la question, voir plus bas). Le cas le plus fréquent, c'est un constructeur, mais ça pourrait être n'importe quelle fonction.

              struct Toto {
                Toto(std::string s) : str(std::move(s)) { }
                std::string str;
              };
              
              struct Titi {
                Titi(const std::string& s) : str(s) { }
                std::string str;
              };
              
              int main() {
                std::string str1("str1");
              
                Toto toto1(str1); // (a)
                Toto toto2("str2"); // (b)
              
                Titi titi1(str1); // (c)
                Titi titi2("str2"); // (d)
              }

              En (a), il y a une copie qui est faite au moment de l'appel, mais dans le constructeur, il n'y a pas de copie, juste un déplacement (ce qui revient à déplacer le pointeur). En (b), il n'y a pas de copie, la chaîne est construite dans le paramètre qui est ensuite déplacé dans la variable membre. En (c), il y a un passage de référence au moment de l'appel, mais il y a une copie dans le constructeur. En (d), il y a une création d'une chaîne sur la pile, puis il y a une copie dans le constructeur. Au final, en passant par valeur, on s'évite une copie si on passe un littéral, alors qu'en passant par référence, on a une copie superflue. C'est pour ça que depuis C++11 (depuis qu'on a la sémantique de déplacement), il est conseillé de passer ce genre de paramètre par valeur, et plus par référence.

              Si on n'a pas besoin de conserver le paramètre, alors il faut passer le paramètre par référence constante. Mais ce cas était déjà géré avant C++11.

              Dans le cas d'un std::shared_ptr<T>, on peut tenir le même genre de raisonnement, mais il y a une subtilité en plus qui est qu'on a en fait deux objets : le pointeur intelligent en lui-même (std::shared_ptr), et l'objet pointé (de type T). Comme le précise Herb Sutter dans l'article que j'ai lié un peu plus haut, si on n'a besoin que de l'objet pointé, on doit passer soit une référence sur un T, soit un pointeur sur un T parce que ça rend la fonction plus générique en ce sens qu'on ne la restreint pas à une seule politique de gestion mémoire. Si on a besoin de partager le pointeur, alors on passe un shared_ptr<T> par copie (pour la même raison que pour le std::string précédemment). Si on passe un shared_ptr<T> par référence non-constante, c'est qu'on veut modifier le pointeur lui-même et pas l'objet pointé. Et si on le passe par référence constante, c'est qu'on ne sait pas si on va faire une copie ou pas, mais ce cas est extrêmement rare et c'est précisément celui qui est donné en exemple dans le post initial.

              • [^] # Re: Godbolt

                Posté par . Évalué à 2.

                C'est pour ça que depuis C++11 (depuis qu'on a la sémantique de déplacement), il est conseillé de passer ce genre de paramètre par valeur, et plus par référence.

                Ce point n'est pas aussi clair. Prends l'exemple suivant :

                void A::set_b(B b) {
                  if(b == this->b) {
                    return;
                  }
                  this->b = b;
                }
                

                et ailleurs dans le code :

                B some_b = ...;
                A* a = ...;
                a->set_b(some_b);
                

                si la variable some_b est égal à a::b, qui est un cas pas délirant du tout dans un contexte classique d'exécution de code, alors tu as une copie de B qui a lieu alors qu'elle n'aurait pas eu si B était passé en référence constante.

                • [^] # Re: Godbolt

                  Posté par . Évalué à 3.

                  Dans ce cas, tu es comme dans le dernier cas que j'ai cité du shared_ptr, tu ne sais pas si tu vas conserver le paramètre ou pas, donc tu passes une référence constante et tu fais la copie seulement si besoin. Donc on est bien d'accord et c'est très logique en fait.

                  • [^] # Re: Godbolt

                    Posté par . Évalué à 2.

                    Vi et faut aussi faire gaffe à ce que la comparaison ne soit pas plus longue que la copie; sinon on perd un peu l'intérêt du schmilblick.

                    Il ne faut pas décorner les boeufs avant d'avoir semé le vent

                  • [^] # Re: Godbolt

                    Posté par . Évalué à 2.

                    Je suis d'accord, c'est juste que je n'ai pas l'impression qu'il y a un consensus clair pour les setter de membre de classe.
                    Personnellement (je ne suis pas une référence, mais j'aime bien donner mon avis quand même :p) je conserve la version "const ref" dans la majorité des cas, et quand j'ai des cas particuliers ou cela peut faire une vraie différence, je me pose la question et je choisi au cas pas cas.

              • [^] # Re: Godbolt

                Posté par . Évalué à 1.

                Oui ok tu ne remet pas en cause ma compréhension des références (ouf ! :) ).
                En fait si tu veux garder une copie, soit elle doit être fait via le passage par valeur soit dans ton code. Le fait de passer une référence ne crée pas une copie en soit. C'est le contenu de la méthode qui la copie.

            • [^] # Re: Godbolt

              Posté par . Évalué à 2.

              Pour garder un exemple dans le style du journal, mais où on sait ce que font les fonctions : https://gcc.godbolt.org/z/2llMlg

              C::f et C::g garde toutes les deux une copie du pointeur, mais f prend par référence et g par valeur. C::h prend possession du pointeur passé par référence de rvalue.

              • c.f(ptr_A) : le pointeur passé par référence puis copié dans la fonction -> 1 copie.
              • c.f(ptr_B) : le pointeur est converti, puis le temporaire est passé par référence, copié et détruit -> 2 copies, 1 destruction.
              • c.g(ptr_A) : le paramètre est crée par le constructeur de copie puis est déplacé par la fonction -> 1 copie, 1 déplacement, 1 destruction d'un pointeur nul (moins cher).
              • c.g(ptr_B) : pareil que le précédent sauf que le constructeur de copie est remplacé par la conversion.
              • c.h(std::move(ptr_A)) : passage par référence, puis affectation -> 1 déplacement.
              • c.h(std::move(ptr_B)) : conversion, passage par référence, puis affectation -> 2 déplacement, 1 destruction d'un pointeur nul.

              J'espère que c'est plus clair. Le passage par référence nécessite une copie supplémentaire pour la conversion, alors que le passage par valeur permet de limiter à une seule copie dans tous les cas.

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.