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