Partage
  • Partager sur Facebook
  • Partager sur Twitter

Const ref ou pas ?

Sujet résolu
    4 octobre 2020 à 21:23:25

    Bonjour. Je débute dans le langage c++ et je me demande s'il est nécessaire d'utiliser des références constantes pour les fonctions ne modifiant pas leurs arguments. De ce que je sais, l'utilisation de références fait que la variable n'est pas copiée lors de l'appel d'une fonction.
    // Ref constante
    int sum(const int &a,const int &b)
    {
      return a+b;
    }
    
    // Ref
    int sum(int &a,int &b)
    {
      return a+b;
    }
    
    // Const
    int sum(const int a,const int b)
    {
      return a+b;
    }
    

    Parmi ces trois fonctions je pense que la première serait plus efficace mais cela est-il nécessaire ?

    Merci d'avance pour vos réponses
     

    • Partager sur Facebook
    • Partager sur Twitter
      5 octobre 2020 à 1:12:30

      On va utiliser

      - const& pour les gros types (intuitivement plus gros que 2 ints, en gros) que l'on reçoit en [in]. C'est aussi ce que l'on va employer avec des templates.
      - & pour les paramètres en [out] ou [in,out]; mais en C++ on va préférer au maximum le retour par valeur -- mais il n'est pas toujours pertinent, cf std::swap.
      - par valeur pour les réceptions en [in] de petits types; ou (sujet avancé, et cela remplace la 1ere "règle" que j'ai évoquée) pour les variables qui seront "copiées" (en vrai, déplacées) dans quelque chose qui va survivre à l'appel de la fonction -- ne te prends pas trop la tête avec ça pour l'instant, c'est un sujet un peu poussé qui à trait à la sémantique dite de déplacement, il y a d'ailleurs une alternative plus précise mais qui demande plu d'huile de coude.

      NB: par valeur et const c'est très bien aussi, c'est la même idée que de chercher à déclarer const les variables locales autant se faire que peut. Par contre on ne le fera que pour les définitions de fonctions. On ne mettra pas le const dans leurs déclarations.

      PS: Dans tes exemples, vu le type int, c'est la 3e version qui est la bonne.

      -
      Edité par lmghs 5 octobre 2020 à 1:14:10

      • Partager sur Facebook
      • Partager sur Twitter
      C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
        5 octobre 2020 à 8:01:15

        Merci beaucoup pour tes explications. Si je résume bien, const:Les variables classiques (int, char, double,.. ) et const & :les objets lourds(strings, vectors,..).
        • Partager sur Facebook
        • Partager sur Twitter
          5 octobre 2020 à 9:22:23

          Bonjour, le mot const ne doit pas être systématique, cela dépend de ce que tu veux faire avec tes paramètres de fonction.

          Lorsque tu déclares un objet ou une variable avec le mot clef const, cela signifie que tu ne pourras pas modifier la valeur de cet objet après sa déclaration.

          C'est un mot clef très puissant pour s'assurer qu'on ne fera pas de bêtises en s'interdissant dès la déclaration de modifier certaines choses.

          Prenons l'exemple d'un professeur qui ferait un programme pour calculer la moyenne de ses élèves. Il stock les notes dans un vector, qu'on ne déclare pas comme constant car on veut ajouter des notes dans notre vector tout au long du trimestre. On veut une fonction qui ajoute une note en testant si elle est bien entre 0 et 20 et on veut une fonction qui calcule la moyenne de toutes les notes. Ca donnerait ceci : 

          #include <iostream>
          #include <vector>
          
          void ajouterNote(std::vector<double> &vec, double note);
          double calculateAverage(const std::vector<double> &vec);
          
          int main() {
              std::vector<double> notes = {12,15,9.5,13}; // Déclaration non constante pour pouvoir ensuite ajouter des notes
          
              ajouterNote(notes,18);    // On peut ajouter des notes
          
              double moyenne = calculateAverage(notes);
          
              std::cout << "moyenne = " << moyenne << std::endl;
          }
          
          void ajouterNote(std::vector<double> &vec, double note)
          {
              // Je ne déclare PAS l'argument de ma fonction comme 'const' car elle DOIT modifier mon vector
              if(note > 20 || note < 0)
                  return;  // Note invalide, on exit la fonction sans rien faire
          
              vec.push_back(note);   // Sinon on ajoute la note
          }
          
          
          double calculateAverage(const std::vector<double> &vec)
          {
              // Je déclare l'argument de ma fonction comme 'const' car elle ne DOIT PAS modifier le vector
              double sum = 0;
          
              for(int k = 0; k < vec.size(); k++) {
                  sum += vec[k];
              }
          
              return (sum / vec.size());
          }
          
          

          Pour savoir si les paramètres de ta fonction doivent être constants, demande toi simplement ce que fais ta fonction avec ses paramètres : est ce qu'elle a seulement besoin de lire et d'utiliser les arguments tels qu'ils sont ou est ce qu'elle a pour but de modifier les paramètres qu'on lui donne

          Ce qui nous ammène à l'utilisation de '&' devant mes paramètres de fonctions. Ce symbole peut avoir 2 utilisations et les deux sont illustrées dans mon exemple d'au dessus.

          Utilisation 1 : Passage d'un gros argument par paramètre pour éviter une copie

          C'est le cas de ma fonction calculateAverage qui passe le vector par référence. Un vector est une grosse structure, on veut éviter que notre fonction en créer une copie alors qu'il existe déjà dans notre mémoire. On le passe donc par référence.

          Globalement, on ne passe PAS par référence les types de bases (int, double, float, bool, char) et on passera tout le reste par référence (string, vector, array, les objets des classes que tu crées ...)

          Utilisation 2 : Quand on veut que notre fonction modifie un de ses arguments 

          C'est le cas de ma fonction ajouterNote. Je veux modifier le vector que je donne à ma fonction. Si je n'avais pas passé mon vector par référence, une copie aurait été créée et ma fonction aurait ajouter la note au vector copié, pas au vector créé dans le main et passé en paramètre. Ce nouveau vector copié et modifié n'aurait existé que dans le corps de ma fonction et aurait disparu une fois ma fonction terminée. Ma fonction n'aurait donc servie à rien.

          Une autre possibilité de modifier un des paramètres donné à une fonction est de faire un return. Si je réécris ma fonction ajouterNote sans passage par référence du vector cela donnerait ça : 

          #include <iostream>
          #include <vector>
          
          std::vector<double> ajouterNote(std::vector<double> vec, double note);
          
          int main() {
              std::vector<double> notes = {12,15,9.5,13}; // Déclaration non constante pour pouvoir ensuite ajouter des notes
          
              notes = ajouterNote(notes,18);    // Je modifie mon vector notes par le nouveau vector renvoyé par la fonction  
          }
          
          
          std::vector<double> ajouterNote(std::vector<double> vec, double note)
          {
              // vec devient une copie de mon vector notes
              if(note > 20 || note < 0)
                  return vec;     // Je renvoie le vector sans modification
                  
              vec.push_back(note);    // J'ajoute la note au vector
              return vec;         // Je renvoie le nouveau vector pour qu'il sois récupéré dans le main
          }
          
          

          Cette solution marche tout à fait mais on retombe sur le "probème" de la copie du vector qui peut-être embêtant si on veut vraiment optimiser son code. De plus, lorsque notre fonction a un type de retour (donc autre que void), il est obligatoire que tous les chemins possibles dans notre fonction renvoient quelque chose de ce type.

          Ici, si la note est invalide, je suis obligé de renvoyé un std::vector<double>. Je ne peux pas simplement afficher un message d'erreur dans la console et sortir de la fonction sans renvoyer de vector. Ce n'est pas gênant dans mon exemple mais c'est quelque chose qu'il faut garder en tête 


          -
          Edité par ThibaultVnt 5 octobre 2020 à 9:43:40

          • Partager sur Facebook
          • Partager sur Twitter
            5 octobre 2020 à 9:24:14

            Non dans ton exemple, le type int est vraiment léger donc tu peux le passer par copie (créer une référence du type int dans les paramètres sera plus consommateur que d'en créer une simple copie). Et vu que c'est une copie, ça n'a pas trop de sens de la déclarer constante. :)

            int sum(int a, int b)
            {
              return a+b;
            }

            En faite c'est les gros objets (à partir du std::string) que tu vas passer par référence (et constante si ta fonction n'est pas censé les modifier).

            -
            Edité par Toniooo 5 octobre 2020 à 9:25:30

            • Partager sur Facebook
            • Partager sur Twitter
              5 octobre 2020 à 12:07:10

              >  le mot const ne doit pas être systématique, cela dépend de ce que tu veux faire avec tes paramètres de fonction.

              Pour du [in], il peut l’être, systématique. D'ailleurs dans `void ajouterNote(std::vector<double> &vec, double note)`, `note`  au singulier pourrait totalement être const. On retrouve la même idée que celle d'avoir nos variables locales const par défaut.

              > Ici, si la note est invalide, je suis obligé de renvoyer un std::vector<double>. Je ne peux pas simplement afficher un message d'erreur dans la console et sortir de la fonction sans renvoyer de vector. Ce n'est pas gênant dans mon exemple mais c'est quelque chose qu'il faut garder en tête 

              Retourner sans signaler que l'action a été ignorée est probablement un des pires choix ici. Cela mérite un contrat restrictif (narrow) (assertion), ou tolérant (wide) (exception). (Ma préférence est que le contrat soit restrictif et la vérification de la "saisie" externe soit faite en amont avant l'appel à ajouterNote, car c'est là que l'on aura le plus de contexte à remonter à l'utilisateur relativement à l'entrée extérieure invalide (quelle ligne du fichier, quel montant saisi dans la boite de dialogue...)

              • Partager sur Facebook
              • Partager sur Twitter
              C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                5 octobre 2020 à 12:15:06

                Toniooo a écrit:

                créer une référence du type int dans les paramètres sera plus consommateur que d'en créer une simple copie


                *Seulement sur un système 64 bits. Sur un système 32 bits, ça reviendra au même puisque un pointeur aura toujours la taille max possible par le système et que les ints, eux, sont encodés sur 32 bits.

                -
                Edité par Raynobrak 5 octobre 2020 à 12:15:35

                • Partager sur Facebook
                • Partager sur Twitter
                  5 octobre 2020 à 12:23:04

                  Salut,

                  Toniooo a écrit:

                  Non dans ton exemple, le type int est vraiment léger donc tu peux le passer par copie (créer une référence du type int dans les paramètres sera plus consommateur que d'en créer une simple copie). Et vu que c'est une copie, ça n'a pas trop de sens de la déclarer constante. :)

                  Oui, mais non...

                  Si l'on défini un paramètre comme étant transmis par valeur, il y aura bel et bien copie de l'argument transmis, et il est donc tout à fait vrai que, quoi que tu  puisse faire avec ton paramètre au niveau de la fonction appelée, la valeur de l'argument d'origine ne sera pas modifiée, par exemple un code proche de

                  void foo(int i){ // i est passé par valeur et donc copié
                      i *= 2;
                      std::cout<<"dans foo(), i vaut "<<i<<"\n";
                  }
                  int main(){
                      int i{4};
                      std::cout<<"avant foo(), i vaut "<<i<<"\n";
                      foo(i);
                      std::cout<<"apres foo(), i vaut "<<i<<"\n";
                  }

                  provoquera un affichage de l'ordre de

                  avant foo(), i vaut 4
                  dans foo(), i vaut 8
                  apres foo(), i vaut 4

                  Par contre, le mot clé const permet de préciser au compilateur que, quoi qu'il arrive, nous ne souhaitons pas être en mesure de modifier la valeur de la donnée (du paramètre, dans le cas présent) concernée.

                  Ainsi, si nous écrivions un code proche de

                  void foo(int const i){ // i est transmis par valeur, mais je
                                         // m'engage à ne pas essayer d'en modifier 
                                         // la valeur
                      i*=2; 
                  }

                  le compilateur va râler (et donc nous afficher une erreur, et refuser de générer le code exécutable) parce que nous ne respectons pas les engagements que nous avons pris (qui sont ... de ne pas essayer de modifier la valeur de i).

                  Ce qu'il faut bien comprendre, c'est que l'appel d'une fonction va faire passer notre programme dans un "contexte secondaire" (par rapport au contexte dans lequel on se trouve au niveau de la fonction appelante) et que la constance (ou non), le fait de pouvoir modifier une donnée dans ce nouveau contexte et la copie (ou non) d'une donnée lorsque l'on entre dans ce nouveau contexte (lorsque l'on appelle une fonction) sont deux choses totalement distinctes.

                  D'un coté, il y a la possibilité (ou non) de modifier l'état de la donnée dans le "contexte secondaire" (dans la fonction appelée), et, de l'autre, il y a la possibilité (ou non) de récupérer les éventuelles modifications apportées à l'intérieur de ce contexte secondaires (si tant est qu'elles aient été autorisées) au niveau du contexte "primaire" (la fonction appelante).

                  S'il y a du sens à interdire la modification d'un paramètre dans la fonction appelée, il est donc "tout à  fait normal" d'imposer cette interdiction en déclarant le paramètre comme constant, fusse-t-il transmis par copie.

                  Bien sur, il y aura d'autant plus de sens à déclarer le paramètre comme étant constant s'il est transmis par référence, et donc si les modifications apportée dans la fonction appelée risquent d'être répercutées au niveau de la fonction appelante, cependant, il faut bien se rendre compte que la constance n'a que pour seul et unique but d'imposer un invariant stricte quant à la manière dont le paramètre pourra être utilisé à l'intérieur de la fonction appelée.

                  Et, à ce titre, je le répète une fois encore, si ton paramètre ne peut pas être modifié, il est parfaitement logique de le déclarer constant, quel que soit la manière dont il a été transmis à la fonction ;)

                  Si, de plus, on ajoute le fait que le compilateur est largement susceptible d'apporter des optimisations auxquelles nous n'aurions pas forcément pensé face à des données déclarées constantes, on se dit qu'il est largement dans notre intérêt de déclarer constant tout ce qui peut l'être :D

                  • Partager sur Facebook
                  • Partager sur Twitter
                  Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                    5 octobre 2020 à 12:32:55

                    Raynobrak a écrit:

                    Toniooo a écrit:

                    créer une référence du type int dans les paramètres sera plus consommateur que d'en créer une simple copie


                    *Seulement sur un système 64 bits. Sur un système 32 bits, ça reviendra au même puisque un pointeur aura toujours la taille max possible par le système et que les ints, eux, sont encodés sur 32bits.

                    Hum... Il y a certes le problème du coût sur la pile, mais le vrai problème, c'est que l'on va toujours payer le prix du déréférencement dès que l'on passe une référence. Et pour des fonctions appelées souvent, et qui ne font in-fine pas grand chose, ce n'est pas toujours négligeable. Il y a de fait un équilibre à trouver entre le coût de la copie et le coût du déréférencement, c'est pour cela que les règles "léger" VS "lourd" sont floues car sur des types un peu limites (genre std::complex<double>) cela va mériter des benchmarks -- si cela concerne des fonctions beaucoup appelées (après mesure). D'autant que les évolutions autour de l'élision de copie peuvent remettre en question des anciennes bonnes pratiques.
                    • Partager sur Facebook
                    • Partager sur Twitter
                    C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                      5 octobre 2020 à 17:35:15

                      Merci beaucoup pour vos réponses. Je vois qu'il faudrait utiliser des références constantes pour des objets lourds destinés à ne pas être modifiés dans la fonction. Pour les autres types, utiliser une référence alors que la fonction ne modifie pas la variable est quasi inutile.
                      • Partager sur Facebook
                      • Partager sur Twitter
                        5 octobre 2020 à 19:23:22

                        * [out] ou [in,out] => référence seule, mais attention on va préférer l'éviter pour les objets copiables et/ou déplaçables, si on le peut afin de  privilégier le retour par valeur.

                        * [in]

                          + si le type modélise une entité (i.e. non copiable, ni déplaçable ; cf FAQ) => référence constante
                          + si le type modélise une valeur (copiable et/ déplaçable)
                             - "petit" type => par valeur
                                ~ NB: "const" valeur a parfaitement du sens, mais c'est assez peu employé
                             - objet dont le duplicata doit survivre à l'appel de la fonction (typique pour des futures valeurs d'attributs passées à des constructeurs ou des setters) => par valeur (pas const!!), et ensuite déplacement avec std::move ; NB: pas un sujet pour débutants
                             - autres cas ("gros" type, et sans vol) => par référence constante.

                        PS: [in] == on regarde le contenu du paramètre, [out] == on modifie/altère l'état du paramètre; [in,out] == on fait les deux. Pas de référence non const dans le cas [in] autrement.

                        -
                        Edité par lmghs 5 octobre 2020 à 19:46:57

                        • Partager sur Facebook
                        • Partager sur Twitter
                        C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                          5 octobre 2020 à 21:18:28

                          en fait, faut bien réfléchir à ce qu'on fait.

                          • Partager sur Facebook
                          • Partager sur Twitter

                          Const ref ou pas ?

                          × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                          × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                          • Editeur
                          • Markdown