TD 2
1 - Encapsulation
1. L'accès public aux variables d'une classe est toujours un élément critique dans un code source : la classe qui contient les données ne peut pas détecter les modifications qui leur sont apportées depuis l'extérieur. D'où la nécessité d'utiliser des accesseurs.
Modifiez votre classe vec2f
pour stocker les coordonnées dans un tableau privé alloué statiquement.
private: // visibilité
float _data[2]; // tableau de flottant alloué statiquement de taille 2
Ajoutez un getter
sur le tableau contenant les coordonnées, par exemple :
float* myvecData = myvector.data();
2. En C++, on peut indiquer au compilateur (et au développeur utilisant votre code) quelles sont les fonctions qui risquent d'entrainer des modifications de l'état interne de la classe. Pour cela, on utilise le mot clef const
après le nom de la fonction, par exemple :
float MyClass::x() const { // La valeur stockée dans mon tableau est dupliquée au
return _data[0]; // changement de contexte lorsque l'on sort de la fonction.
} // L'utilisateur n'a aucun accès à la variable interne, juste à une copie.
Le fait de ne pas utiliser le mot clef const
dans ce cas là ne cause pas d'erreur de compilation. Cependant cela permet au développeur ayant un accès en lecture seule à une instance de pouvoir quand même appeler l'accesseur, chose impossible sinon :
const MyClass A;
A.x(); // erreur de compilation si x n'est pas déclarée comme const
Créez les accesseurs (getters
, setters
) adéquates respectant les accès constants. Modifiez le code utilisant votre classe vec2f
pour tester vos modifications. Que se passe-t-il si vous utilisez le mot clef const
pour caractériser un setter
?
3. En C++ il est tout à fait possible de retourner un pointeur constant :
float *const data() const;
getter
en permettant via ce pointeur constant de modifier les valeurs du tableau.
2 - Les références
Si l'on reprend l'exercice précédent, on a d'un côté le passage par pointeur, de l'autre le passage par copie. Dans le premier cas, on retourne un accès à l'emplacement mémoire, donc on ne fait pas de copie mais on casse l'encapsulation. Dans le second, on sécurise bien l'accès au données, par contre cela nécessite une copie.
Cette copie est relativement négligeable quand il s'agit de retourner un float
comme c'est le cas ici, mais peut être très coûteuse lorsque l'on retourne un objet plus volumineux (comme un tableau par exemple). Cela peut devenir très problématique lorsque les performances sont primordiales (pour des applications temps-réel comme les jeux-vidéo), ou lorsque la quantité de mémoire disponible est faible (on stocke à un moment donné deux fois le même objet).
Heureusement, il existe en C++ un outil permettant de donner des accès directs à l'espace mémoire tout en contrôlant le niveau d'accès (lecture ou lecture/écriture), sans faire de copie mémoire : c'est le passage par référence, noté &
.
1. Modifiez vos getters
pour que la valeur de retour ne soit plus recopiée. Attention à préserver un accès en lecture seule pour la valeur retournée. Ajoutez dans cette fonction un message sur la sortie standard indiquant que cette méthode a été appelée (par example vec2f: constant access requested
).
2. Il peut parfois être utile de pouvoir modifier l'état d'un objet directement sans passer par un setter
(mais toujours en informant la classe), comme par exemple en faisant :
vec2f coord;
coord.x() = 10; // écriture directe dans la variable
coord.y() = 10;
Ajoutez ces fonctions à votre classe de vecteur en utilisant le passage par référence. Ajoutez y les messages sur la sortie standard, en précisant l'accès en écriture (par example vec2f: read access requested
).
3. Le passage par référence peut aussi être utilisé pour transmettre des paramètres à une fonction. Cela permet non seulement de ne pas recopier certains objets volumineux tout en les verrouillant en lecture seule (passage par référence constante), mais aussi de pouvoir autoriser l'accès en écriture sur d'autres. Une fonction peut ainsi modifier plusieurs objets différents, sans être limitée à une seule valeur de retour.
Écrivez une méthode div
prenant en paramètre un flottant divisant l'instance courante et un booleen en lecture/écriture qui sera mis à false
en cas de division par zéro. La fonction doit retourner le vecteur résultat. Cette méthode doit utiliser uniquement les méthodes ajoutées à la question précédente pour accéder aux coordonnées du vecteur (retour par référence constante ou non).
4. Dans cette méthode, essayez d'écrire dans le flottant en paramètre passé en référence constante. Que se passe-t-il ? Observez aussi les sorties de debug dans la console et l'utilisation des méthodes en lecture seule ou en écriture.
3 - Surcharge d'opérateur
Notre classe vecteur peut encore être complétée pour ajouter des opérateurs mathématiques (addition, soustraction, produit scalaire, etc.)
1. En C++ il est possible de surcharger certains opérateurs, c'est à dire d'en redéfinir le fonctionnement pour une classe donnée. Ajoutez dans votre fichier entête (vec2.h) un opérateur +
, externe à la déclaration de la classe vec2f
:
vec2f operator+(const vec2f& a, const vec2f& b);
L'usage des opérateurs externes est très utile lorsque l'on n'a pas accès au code source définissant les classes sur lesquelles on travaille. Testez l'opérateur dans votre code.
Liste des opérateurs C++ : http://en.wikipedia.org/wiki/Operators_in_C_and_C++
2. Lorsque l'accès au code source est possible (comme ici), les opérateurs peuvent être définis directement dans la classe. Ajoutez un opérateur -
interne à la classe. Profitez-en pour ajouter l'opérateur de multiplication par un scalaire.
3. Ajoutez un opérateur crochet pour accéder directement aux valeurs du tableau contenant les coordonnées.
4. Ajoutez un opérateur de flux permettant de produire une sortie de ce type [0.5, 0.1] via l'appel :
std::cout << myVector << std::endl;
Notez que le premier paramètre de l'opérateur de flux est un objet de type stream
, classe de la STL auquelle vous n'avez pas accès. Par conséquent, où la redéfinition de cet opérateur doit-elle avoir lieu ?