Copy / Reference **************** .. include:: ../BoutonGoogleTrad.rst La première question à se poser =============================== **Que se passe-t-il lorsqu'on écrit** : :cpp:`A = B` ? .. panels:: :column: col-lg-10 p-2 Pour les types fondamentaux (:cpp:`int, double...`) et pour la plupart des objets : * :cpp:`A = B` copie le contenu de :cpp:`B` vers :cpp:`A`. * Ensuite, les deux variables évoluent indépendamment : modifier l'une ne change pas l'autre. .. warning:: Il existe une situation particulière lorsque les objets :cpp:`A` et :cpp:`B` vont partager des ressources comme un fichier ou un segment de mémoire. Dans cette configuration, l'indépendance entre ces deux objets est compromise. Nous allons dans la suite tester ces différents scénarios. Exemple : cas des structures ---------------------------- Veuillez exécuter le code suivant, analysez ensuite les affichages. .. code-block:: cpp #include struct Vec { int x; int y; }; int main() { // struct type Vec B; B.x = 10; B.y = 20; Vec A; A = B; // the case we are interested in A.x += 5; A.y += 5; // output std::cout << B.x << " " << B.y << std::endl; std::cout << A.x << " " << A.y << std::endl; } .. quiz:: affectationStruct :title: Affectations et structure Indiquez si l'affirmation suivante est vraie ou fausse : .. csv-table:: :widths: 10 :delim: ! :quiz:`{"type":"TF","answer":"T"}` L'écriture :cpp:`A=B` avec des structures produit une copie. Passage d'arguments par valeur ============================== .. panels:: :column: col-lg-10 p-2 **REGLE** : En C++, le passage d’un argument par valeur crée une copie de l’argument dans le paramètre de la fonction. Cas des types fondamentaux -------------------------- Nous allons vérifier ce qu'il se passe en exécutant le code suivant et en analysant les résultats. .. code-block:: cpp #include void F(int a, double b, bool c) { std::cout << "Function entry : " << a << " " << b << " " << c << std::endl; a += 1; b += 1; c = !c; std::cout << "Function exit : " << a << " " << b << " " << c << std::endl; } int main() { // integer, double, bool types int x = 10; double y = 20.0; bool flag = true; std::cout << "Before call : " << x << " " << y << " " << flag << std::endl; F(x, y, flag); std::cout << "After call : " << x << " " << y << " " << flag << std::endl; } .. quiz:: FondParamFnt :title: Passage de paramètres et types fondamentaux Pour chaque type testé, indiquez si l'affirmation correspondante est vraie ou fausse : .. csv-table:: :widths: 10 :delim: ! :quiz:`{"type":"TF","answer":"T"}` Le passage de paramètres avec un type entier produit une copie. :quiz:`{"type":"TF","answer":"T"}` Le passage de paramètres avec un type double produit une copie. :quiz:`{"type":"TF","answer":"T"}` Le passage de paramètres avec un type booléen produit une copie. Cas des structures ------------------ Veuillez exécuter le code suivant, puis analysez les résultats. .. code-block:: cpp #include struct Vec { int x; int y; }; void F(Vec v) { std::cout << "Function entry : " << v.x << " " << v.y << std::endl; v.x += 1; v.y += 2; std::cout << "Function exit : " << v.x << " " << v.y << std::endl; } int main() { // struct type Vec data; data.x = 10; data.y = 20; std::cout << "Before call : " << data.x << " " << data.y << std::endl; F(data); std::cout << "After call : " << data.x << " " << data.y << std::endl; } .. quiz:: ParamStruct :title: Passage de paramètres et structure Indiquez si l'affirmation suivante est vraie ou fausse : .. csv-table:: :widths: 10 :delim: ! :quiz:`{"type":"TF","answer":"T"}` Le passage par valeur d’un argument de type structure provoque la construction du paramètre par copie de l'argument. Rvalue et lvalue ================ Introduction ------------ Le langage C a posé les bases d'une classification des expressions basée sur deux grandes catégories : les :cppterm:`lvalues` et les :cppterm:`rvalues`. Pour introduire ces notions, prenons un exemple : .. code-block:: cpp int T[10]; T[3] = 7; L'élément sur la gauche du signe :cpp:`=`, n'est pas un nom de variable, l'écriture :cpp:`T[i]` est plus complexe et correspond à une expression, ainsi on trouve : * Un index :cpp:`3` * Le nom d'un container :cpp:`T` * L'utilisation de l'opérateur :cpp:`[]` Cette expression désigne la 4-ème case du tableau :cpp:`T`, un endroit précis en mémoire. Dans cette logique, le langage C++ distingue deux grandes catégories d’expressions : * :cppterm:`lvalue` (left) : est une expression qui désigne un objet précis en mémoire. * :cppterm:`rvalue` (right) : est une expression qui représente plutôt une valeur calculée ou produite. Historiquement, on expliquait ces notions ainsi : * A gauche d'une affectation, on trouve des lvalues uniquement * Une rvalue ne peut se trouver qu'à droite d'une affectation Quelques remarques : * Une lvalue peut apparaître à droite comme à gauche d'une affectation. * Une expression contenant des calculs ou des valeurs comme :cpp:`T[3+i-k]` peut cependant correspondre à une lvalue. Exemples de rvalues ------------------- .. code-block:: cpp // Numeric literals 5 3.14 // Results of arithmetic expressions a + b // Value returned by a function max(6,5) // post-increment n++; // comparison operator a < b Exemples de lvalues ------------------- .. code-block:: cpp x = ... T[3] = ... p.x = ... Quizzz ------ .. quiz:: rvaluelvalue :title: lvalue et rvalue. Indiquez si chaque affirmation est vraie ou fausse : .. csv-table:: :widths: 10 :delim: ! :quiz:`{"type":"TF","answer":"F"}` Lvalue et rvalue correspondent à des types :quiz:`{"type":"TF","answer":"T"}` Une lvalue peut se situer à droite et à gauche dans une affectation :quiz:`{"type":"TF","answer":"T"}` Une rvalue peut se situer uniquement à droite dans une affectation :quiz:`{"type":"TF","answer":"F"}` Une lvalue se définit par le caractère éphémère du résultat qu'elle représente :quiz:`{"type":"TF","answer":"T"}` Une lvalue permet de stocker un résultat :quiz:`{"type":"TF","answer":"F"}` L'expression :cpp:`a+b` désigne une lvalue Référence de variables ====================== Introduction ------------ Pour créer une référence (:cppterm:`reference`) vers une variable existante, il suffit d'utiliser la syntaxe suivante : .. panels:: :column: col-lg-10 p-2 **SYNTAXE** - Création d'une référence :cpp:`Type & RefName = lvalue;` Une **référence** définit un alias vers un élément stocké en mémoire de façon pérenne (lvalue). Nous pouvons définir des références sur des variables mais aussi sur des lvalues plus complexes. Voici un exemple : .. code-block:: cpp #include int main() { int a = 7; int & refA = a; // definition of a reference to variable a std::cout << refA; // ==>> 7 int arr[4] = { 1, 2, 3, 4}; int & refElem = arr[2]; // definition of a reference to arr[2] std::cout << refElem; // ==>> 3 } .. warning:: Lors de la création d'une référence, aucune copie n'est effectuée. La référence et l'élément original désignent une même chose. Utiliser l'une ou l'autre est équivalent. La syntaxe nous interdit de créer une référence sans l'initialiser. Ainsi, on ne peut pas écrire : .. code-block:: cpp int main() { int & ref; // ==> not possible, a reference must always be initialized during definition } Vérification ------------ Veuillez exécuter le code suivant puis analyser les résultats et confirmer que la référence n'effectue pas de copie : .. code-block:: cpp #include struct Vec { int x; int y; }; int main() { // int type int n = 10; int & refN = n; refN++; std::cout << n << " " << refN << std::endl; // struct type Vec obj; obj.x = 10; obj.y = 20; Vec & alias = obj; alias.x += 5; alias.y += 5; std::cout << alias.x; } Référence et passage d'arguments ================================ Un paramètre de fonction qui correspond à une référence ne définit pas une nouvelle variable mais un alias renommant la donnée passée en argument. Par conséquent, toutes les modifications effectuées sur la référence sont en fait effectuées sur la donnée passée en argument. Voici la syntaxe : .. panels:: :column: col-lg-10 p-2 **SYNTAXE** - Passage d'un argument par référence : :cpp:`... FunctionName(VarType & RefName, ...) { ... }` .. note:: L'utilisation de références dans les paramètres de fonction permet de retourner plusieurs informations. L'instruction :cpp:`return` ne permet elle que de retourner un unique élément ce qui peut parfois être limité. Veuillez exécuter le code suivant puis analyser les résultats et confirmer qu'aucune copie n'est effectuée : .. code-block:: cpp #include struct Vec { int x; int y; }; void F(int & n, Vec & obj) { n += 1; obj.x += 2; obj.y += 3; } int main() { // int type int a = 10; // struct type Vec A; A.x = 10; A.y = 20; std::cout << "Avant l'appel : " << a << " / " << A.x << " " << A.y << std::endl; F(a,A); std::cout << "Après l'appel : " << a << " / " << A.x << " " << A.y << std::endl; } Exercices --------- .. panels:: Fonctions disponibles ^^^^^^^^^^^^^^^^^^^^^ .. code-block:: cpp int F(int t) { t += 2; return t; } void I(int & t) { t += 7; } int Z(int & t) { t = 1; return 5; } int F(int & a, int & b) { return a+b; } --- Exercice 1 .. code-block:: cpp int main() { int a; Z(a); I(a); std::cout << a; } --- Exercice 2 .. code-block:: cpp int main() { int a = 8; a += F(a); I(a); std::cout << a; } --- Exercice 3 .. code-block:: cpp int main() { int a = 8; std::cout << I(a+8); } --- Exercice 4 .. code-block:: cpp int main() { int a = 8; int b = 3; std::cout << F(a,b); } --- Exercice 5 .. code-block:: cpp int main() { int a = 8; int b = 3; std::cout << F(F(b,a)); } .. quiz:: RefTest :title: Premiers pas avec les références. Pour chaque exemple, indiquez l'affichage obtenu ou ERR si le programme émet une erreur : .. csv-table:: :delim: ! Exercice 1 :quiz:`{"type":"FB","answer":"8"}` ! Exercice 4 :quiz:`{"type":"FB","answer":"11"}` Exercice 2 :quiz:`{"type":"FB","answer":"25"}` ! Exercice 5 :quiz:`{"type":"FB","answer":"13"}` Exercice 3 :quiz:`{"type":"FB","answer":"ERR"}` Exercices --------- .. panels:: Fonctions disponibles ^^^^^^^^^^^^^^^^^^^^^ .. code-block:: cpp int F(int & t) { t += 2; return t; } void I(int & t) { t += 7; } int & Z(int & t) { t = 0; return t; } int & F(int & a, int & b) { return a; } --- Exercice 6 .. code-block:: cpp int main() { int a = 8; int b = 3; std::cout << F(F(b,a)); } --- Exercice 7 .. code-block:: cpp int main() { int a = 8; std::cout << Z(a+1); } --- Exercice 8 .. code-block:: cpp int main() { int a = 8; int b = 3; Z(F(a,b))++; std::cout << a; } --- Exercice 9 .. code-block:: cpp int main() { int a = 8; std::cout << F(F(a)); } --- Exercice 10 .. code-block:: cpp int main() { int a = 3; int b = 2; F(Z(a),Z(b))++; std::cout << a; } .. quiz:: RefTest :title: Exercices sur les références - Niveau 2. Pour chaque exemple, indiquez l'affichage obtenu ou :cpp:`ERR` si le programme émet une erreur : .. csv-table:: :delim: ! Exercice 6 :quiz:`{"type":"FB","answer":"5"}` ! Exercice 9 :quiz:`{"type":"FB","answer":"ERR"}` Exercice 7 :quiz:`{"type":"FB","answer":"ERR"}` ! Exercice 10 :quiz:`{"type":"FB","answer":"1"}` Exercice 8 :quiz:`{"type":"FB","answer":"1"}` Pièges classiques ================= Certains scénarios peuvent facilement conduire à des comportements inattendus. Nous les présentons pour vous aider à les connaître. Performance ----------- Lorsque l'on transmet un élément prenant de la place mémoire, il est préférable de le passer par référence afin d'éviter une recopie des données. Cependant, si vous effectuez un passage par copie, vous n'aurez pas de message d'erreur, mais vous pourrez constater un ralentissement du programme. Prenons l'exemple de la fonction transposition d'une matrice : .. code-block:: cpp Matrix Transpose(Matrix M) { Matrix result; ... return result; } Cette fonction compile sans erreur et fonctionne correctement. Cependant, l'écriture **Matrix M** pose problème car elle déclenche une copie (passage par valeur), ce qui représente un traitement inutile. Il faut donc préférer un passage par référence : .. code-block:: cpp Matrix Transpose(Matrix & M) { Matrix result; ... return result; } .. panels:: :column: col-lg-10 p-2 **REGLE** : Il est préférable de passer les gros objets par référence plutôt que par valeur, ceci afin d’éviter des copies inutiles. Retour de référence depuis une variable locale ---------------------------------------------- Techniquement, vous pouvez définir une fonction retournant une référence : .. code-block:: cpp int & Test(int a) { int b = a * a + 3; return b; // ERROR: returning reference to a local variable } La syntaxe de ce programme est correcte. Le code compile, pourtant son exécution produit une erreur système inconnue. Voici ce que l'on constate : * Dans la fonction :cpp:`Test()`, nous créons une variable locale :cpp:`b` * Nous retournons ensuite une référence vers cette variable * L'exécution du corps de la fonction se termine et la variable :cpp:`b` est détruite Ainsi, la fonction appelante, reçoit une référence vers une variable "morte". Le comportement du programme devient **indéterminé** :cppterm:`undefined behavior` dans ce genre de situation. Des scénarios très différents peuvent se produire : * Tout se passe bien - 1% de chance * Le programme peut crasher avec ou sans message - souvent sans * Une autre variable peut être modifiée à la place de :cpp:`b`, produisant un bug incompréhensible plus tard dans le programme * Une partie de la mémoire contenant du code peut être modifiée ce qui fait que le programme peut se bloquer .. panels:: :column: col-lg-10 p-2 **REGLE** : On ne doit pas retourner une référence sur une variable locale à une fonction. Une syntaxe d'appel ambiguë --------------------------- Nous présentons dans le code ci-dessous, un passage par copie et un passage par référence : .. code-block:: cpp #include void F1(int n) { n += 1; std::cout << n << std::endl; } void F2(int &n) { n += 1; std::cout << n << std::endl; } int main() { int a = 5; F1(a); // pass by value F2(a); // pass by reference } Examinons la définition des deux fonctions disponibles, l'une utilise un passage par copie et l'autre par référence visible grâce au signe :cpp:`&`. Examinons maintenant les appels dans la fonction :cpp:`main()`, nous avons : :cpp:`F1(a)` et :cpp:`F2(a)`, on ne constate aucune différence de syntaxe. .. panels:: :column: col-lg-10 p-2 **ATTENTION** : En examinant un appel de fonction, il est impossible de déterminer en C++ si les variables sont passées par copie ou par référence. C'est une faiblesse du langage. Exercices --------- .. quiz:: RefTest222 :title: Les cas complexes Pour chaque exemple, indiquez l'affirmation est vraie ou fausse : .. csv-table:: :delim: ! :quiz:`{"type":"TF","answer":"F"}` Si une fonction retourne une référence sur une variable locale, cela la maintient en vie. :quiz:`{"type":"TF","answer":"F"}` L'écriture :cpp:`F(a)` correspond uniquement à un passage par copie. :quiz:`{"type":"TF","answer":"T"}` Le passage par référence permet de limiter le temps passé à faire des copies. Préparation à l'examen ====================== Il est vivement recommandé de ne pas déléguer ces exercices à l'IA car ils seront repris lors de l'examen écrit. Exercice 1 ---------- * Codez : * Une fonction :cpp:`void inc(...)` qui incrémente l'entier passée en paramètre * Une fonction :cpp:`void makePositive(...)` qui prend un entier et le transforme en nombre positif * Une fonction :cpp:`void rotate(...)` qui prend trois entiers :cpp:`a,b,c` et les décale sur la gauche :cpp:`a←b`... * Une fonction :cpp:`void clamp(a,b,c)` qui fait en sorte que :cpp:`a=b` si :cpp:`ac` * Intégrez les fonctions de test suivantes dans votre code * Validez vos fonctions .. code-block:: cpp #include #include using namespace std; int main() { // ---- inc ---- int x = 0; inc(x); if (x != 1) cout << "[FAIL] inc: got=" << x << " expected=1\n"; x = -10; inc(x); if (x != -9) cout << "[FAIL] inc: got=" << x << " expected=-9\n"; // ---- makePositive ---- x = -3; makePositive(x); if ( x != 3 ) cout << "[FAIL] makePositive: got=" << x << " expected=3\n"; x = 12; makePositive(x); if ( x != 12 ) cout << "[FAIL] makePositive: got=" << x << " expected=12\n"; // ---- rotate ---- int a=1,b=2,c=3; rotate(a,b,c); if (!(a==2 && b==3 && c==1)) cout << "[FAIL] rotate (1,2,3): got=("<