Copy / Reference


To translate this course in your native langage:

  • In Chrome: right-click anywhere on the page and select “Translate to…”

  • Or use this language selector:

Les mots colorés en anglais représentent des mots-clés de la norme du langage C++. Ils sont conservés à l’identique lors de l’utilisation d’une traduction automatique avec Google Translate. Exemple : l’héritage (inheritance) est un principe fondamental de C++.


La première question à se poser

Que se passe-t-il lorsqu’on écrit : A = B ?

Pour les types fondamentaux (int, double…) et pour la plupart des objets :

  • A = B copie le contenu de B vers A.

  • Ensuite, les deux variables évoluent indépendamment : modifier l’une ne change pas l’autre.

Avertissement

Il existe une situation particulière lorsque les objets A et 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.

#include <iostream>

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

Indiquez si l’affirmation suivante est vraie ou fausse :

L’écriture A=B avec des structures produit une copie.

Passage d’arguments par valeur

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.

#include <iostream>

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

Pour chaque type testé, indiquez si l’affirmation correspondante est vraie ou fausse :

Le passage de paramètres avec un type entier produit une copie.

Le passage de paramètres avec un type double produit une copie.

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.

#include <iostream>

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

Indiquez si l’affirmation suivante est vraie ou fausse :

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 lvalues et les rvalues. Pour introduire ces notions, prenons un exemple :

int T[10];
T[3] = 7;

L’élément sur la gauche du signe =, n’est pas un nom de variable, l’écriture T[i] est plus complexe et correspond à une expression, ainsi on trouve :

  • Un index 3

  • Le nom d’un container T

  • L’utilisation de l’opérateur []

Cette expression désigne la 4-ème case du tableau T, un endroit précis en mémoire. Dans cette logique, le langage C++ distingue deux grandes catégories d’expressions :

  • lvalue (left) : est une expression qui désigne un objet précis en mémoire.

  • 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 T[3+i-k] peut cependant correspondre à une lvalue.

Exemples de rvalues

// 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

x = ...

T[3] = ...

p.x = ...

Quizzz

Indiquez si chaque affirmation est vraie ou fausse :

Lvalue et rvalue correspondent à des types

Une lvalue peut se situer à droite et à gauche dans une affectation

Une rvalue peut se situer uniquement à droite dans une affectation

Une lvalue se définit par le caractère éphémère du résultat qu’elle représente

Une lvalue permet de stocker un résultat

L’expression a+b désigne une lvalue

Référence de variables

Introduction

Pour créer une référence (reference) vers une variable existante, il suffit d’utiliser la syntaxe suivante :

SYNTAXE - Création d’une référence

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 :

#include <iostream>

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
    }

Avertissement

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 :

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 :

#include <iostream>

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 :

SYNTAXE - Passage d’un argument par référence :

… FunctionName(VarType & RefName, …) { … }

Note

L’utilisation de références dans les paramètres de fonction permet de retourner plusieurs informations. L’instruction 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 :

#include <iostream>

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

Fonctions disponibles

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

int main()
{
        int a;
        Z(a);
        I(a);
        std::cout << a;
}

Exercice 2

int main()
{
    int a = 8;
        a += F(a);
        I(a);
        std::cout << a;
}

Exercice 3

int main()
{
        int a = 8;
        std::cout << I(a+8);
}

Exercice 4

int main()
    {
            int a = 8;
            int b = 3;
            std::cout << F(a,b);
    }

Exercice 5

int main()
{
        int a = 8;
        int b = 3;
        std::cout << F(F(b,a));
}

Pour chaque exemple, indiquez l’affichage obtenu ou ERR si le programme émet une erreur :

Exercice 1

Exercice 4

Exercice 2

Exercice 5

Exercice 3

Exercices

Fonctions disponibles

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

int main()
{
        int a = 8;
        int b = 3;
        std::cout << F(F(b,a));
}

Exercice 7

int main()
{
  int a = 8;
  std::cout << Z(a+1);
}

Exercice 8

int main()
{
        int a = 8;
        int b = 3;
        Z(F(a,b))++;
        std::cout << a;
}

Exercice 9

int main()
{
        int a = 8;
        std::cout << F(F(a));
}

Exercice 10

int main()
{
        int a = 3;
        int b = 2;
        F(Z(a),Z(b))++;
        std::cout << a;
}

Pour chaque exemple, indiquez l’affichage obtenu ou ERR si le programme émet une erreur :

Exercice 6

Exercice 9

Exercice 7

Exercice 10

Exercice 8

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 :

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 :

Matrix Transpose(Matrix & M)
{
        Matrix result;
        ...
        return result;
}

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 :

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 Test(), nous créons une variable locale b

  • Nous retournons ensuite une référence vers cette variable

  • L’exécution du corps de la fonction se termine et la variable 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é 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 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

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 :

#include <iostream>

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 &. Examinons maintenant les appels dans la fonction main(), nous avons : F1(a) et F2(a), on ne constate aucune différence de syntaxe.

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

Pour chaque exemple, indiquez l’affirmation est vraie ou fausse :

Si une fonction retourne une référence sur une variable locale, cela la maintient en vie.

L’écriture F(a) correspond uniquement à un passage par copie.

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 void inc(…) qui incrémente l’entier passée en paramètre

    • Une fonction void makePositive(…) qui prend un entier et le transforme en nombre positif

    • Une fonction void rotate(…) qui prend trois entiers a,b,c et les décale sur la gauche a←b…

    • Une fonction void clamp(a,b,c) qui fait en sorte que a=b si a<b et a=c si a>c

  • Intégrez les fonctions de test suivantes dans votre code

  • Validez vos fonctions

#include <iostream>
#include <cmath>

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=("<<a<<","<<b<<","<<c<<") expected=(2,3,1)\n";

        a=7,b=8,c=9;
        rotate(a,b,c); rotate(a,b,c); rotate(a,b,c);
        if (!(a==7 && b==8 && c==9))
                cout << "[FAIL] rotate x3: got=("<<a<<","<<b<<","<<c<<") expected=(7,8,9)\n";

        // ---- clamp ----
        int v = 3; clamp(v, 0, 5);
        if (v != 3) cout << "[FAIL] clamp: got="<<v<<" expected=3\n";

        v = -10; clamp(v, -3, 7);
        if (v != -3) cout << "[FAIL] clamp: got="<<v<<" expected=-3\n";

        v = 99; clamp(v, -3, 7);
        if (v != 7) cout << "[FAIL] clamp: got="<<v<<" expected=7\n";

        v = 5; clamp(v, 5, 10);
        if (v != 5) cout << "[FAIL] clamp: got="<<v<<" expected=5\n";

        v = 10; clamp(v, 5, 10);
        if (v != 10) cout << "[FAIL] clamp: got="<<v<<" expected=10\n";

        return 0;
}