ELCa11 - BE #3 — Surcharge d’opérateurs et classes génériques¶
Cours associé : Cours 3 — Surcharge d’opérateurs et classes génériques (voir le polycopié, section Cours 3).
Objectifs¶
- Surcharger des opérateurs sur la classe
DamierDyn(opérateurs+,+=,<<). - Comprendre la sémantique de retour des opérateurs (par valeur vs par référence).
- Distinguer méthode membre et fonction libre (
friend). - Découvrir les classes templates pour généraliser le type des éléments.
Prérequis¶
- BE #2 : classe
DamierDynavec constructeur, destructeur, constructeur de recopie,operator=.
Principe¶
Le BE comporte deux parties :
- Partie 1 — Surcharge des opérateurs
+,+=,<<surDamierDyn. - Partie 2 — Création d’une classe générique
DamierDynG<G>qui paramètre le type des cases.
Note : l’opérateur
=a déjà été écrit au BE #2 ; on ne le refait pas ici.
Partie 1 — Surcharge d’opérateurs¶
Notions à maîtriser
- Opérateur en tant que méthode membre ou fonction libre ;
- sémantique de retour :
- opérateur binaire qui produit un nouvel objet (
+) → retour par valeur ; - opérateur qui modifie l’objet à gauche (
+=) → retour par référence ;
- opérateur binaire qui produit un nouvel objet (
- surcharge multiple d’un même opérateur ;
- mot-clé
friend; - réutilisation : implémenter
+à partir de+=.
Contexte¶
Vous reprenez la classe DamierDyn du BE #2. Le but est de permettre l’écriture suivante :
DamierDyn d1(3, 5, 7), d2(3, 5, 2);
DamierDyn d3 = d1 + d2; // somme case par case (étape 1)
d1 += d2; // ajoute d2 à d1 (étape 2)
d1 += 3; // ajoute 3 à toutes les cases (étape 3)
std::cout << d1; // affiche d1 (étape 4)
Comportement en cas d’incompatibilité de dimensions¶
Pour + et += entre deux damiers, les dimensions doivent être égales. Si elles ne le sont pas, afficher un message d’erreur sur std::cerr (avec le nom de la fonction concernée) puis appeler std::exit(1) :
#include <cstdlib> // pour std::exit
// ...
std::cerr << __func__ << " — dimensions incompatibles." << std::endl;
std::exit(1);
Aparté —
__func__est une constante prédéfinie du C++ qui contient le nom de la fonction courante. Très pratique pour des messages d’erreur informatifs.Important :
std::exit(1)est une solution brutale — elle termine le programme sans appeler les destructeurs des objets en cours, ce qui peut provoquer des fuites de ressources. On reviendra sur cette gestion d’erreur au BE #4 avec les exceptions, qui résolvent proprement ce problème.
Étape 1 — operator+¶
Signature attendue (méthode membre, méthode const) :
Pourquoi cette signature ?
- Retour par valeur (
DamierDyn, pasDamierDyn&) : le résultat est un nouvel objet. Le retourner par référence pointerait vers une variable locale détruite à la sortie de la fonction (catastrophe). - Paramètre
const&: pas de copie deD, lecture seule. constà la fin : la méthode ne modifie pas l’objet à gauche du+.
Implémentation :
- vérifier que les dimensions de
*thisetDsont égales (sinoncerr+exit(1)) ; - créer un nouveau
DamierDynaux mêmes dimensions ; - remplir ses cases par
T[i][j] + D.T[i][j]; - retourner ce nouveau damier.
Vous pouvez utiliser une méthode privée bool sameDimensions(const DamierDyn& D) const; pour factoriser le test.
Étape 2 — operator+= avec un autre damier¶
Signature attendue :
Pourquoi cette signature ?
- Retour par référence (
DamierDyn&) : c’est l’idiome standard, qui permet d’écrire(d1 += d2) += d3;oud1 = d2 += d3;. - Pas de
constà la fin : la méthode modifie*this.
Implémentation : vérification des dimensions, ajout case par case, retour de *this.
Étape 3 — operator+= avec un entier (surcharge)¶
Signature attendue :
Cette surcharge porte le même nom que la précédente, mais une signature différente. Le compilateur choisira la bonne version selon le type de l’argument :
Conseil de factorisation : une fois
operator+=écrit, vous pouvez réécrireoperator+en termes de+=:
Étape 4 — operator<< (sortie standard)¶
Signature attendue (fonction libre, pas méthode membre) :
Pourquoi pas une méthode membre ? Une méthode membre est implicitement de la forme objet.methode(args). Si operator<< était membre, on devrait écrire d << std::cout, ce qui n’est pas l’usage attendu (std::cout << d). Pour que std::cout se trouve à gauche de l’opérateur, operator<< doit être une fonction libre dont le premier paramètre est le flux.
Pourquoi friend ? Une fonction libre n’a pas accès aux membres private de la classe. Le mot-clé friend (déclaré dans la classe) lui accorde cet accès.
Implémentation : itérer sur les cases et écrire chacune dans le flux (avec un format aligné via <iomanip>).
std::ostream& operator<< (std::ostream& os, const DamierDyn& D) {
for (int i = 0; i < D.L; ++i) {
for (int j = 0; j < D.C; ++j) {
os << std::setw(4) << D.T[i][j];
}
os << std::endl;
}
return os;
}
Le retour std::ostream& permet l’enchaînement : std::cout << d1 << d2;.
Programme de test (Partie 1)¶
Dans main(), vérifier les comportements suivants :
- créer deux damiers
D1etD2aux mêmes dimensions, avec des valeurs initiales différentes ; - tester
D3 = D1 + D2;et afficherD3; - tester
D1 += D2;et afficherD1; - tester
D1 += 5;et afficherD1; - tester
std::cout << D1 << D2;(enchaînement) ; - tester
D4 = D1 + D2 + D3;(enchaînement de+) ; - tester avec deux damiers de dimensions différentes : vérifier que le programme s’arrête avec un message sur
cerr.
Points à noter¶
- Le retour par valeur de
operator+peut sembler coûteux (copie d’un objet potentiellement gros). Le compilateur applique automatiquement une optimisation appelée Return Value Optimization (RVO) qui élimine cette copie dans la plupart des cas. - L’idiome standard est : opérateurs modificateurs (
+=,-=,*=) en méthodes membres retournant*this&; opérateurs producteurs (+,-) souvent écrits en termes des modificateurs. operator<<ne peut pas être déclaréconstà la fin (c’est une fonction libre, pas une méthode).
Questions à se poser¶
- Pourquoi
operator+retourne-t-il par valeur, alors queoperator+=retourne par référence ? - Que se passerait-il si on déclarait
operator+sansconstà la fin ? - Pourquoi
operator<<doit-il être une fonction librefriendplutôt qu’une méthode membre ? - Si vous écrivez
d3 = d1 + d2 + d3;, combien de fois le constructeur de recopie est-il appelé (sans optimisation du compilateur) ?
Partie 2 — Classes génériques (templates)¶
Notions à maîtriser
- Définition d’un template de classe :
template <class G> class Foo - Type paramétré
Gutilisé comme un type concret dans la classe ; - particularité technique : code complet dans le
.h; - valeur par défaut générique avec
G().
Contexte¶
La classe DamierDyn ne manipule que des entiers. Pour manipuler un damier de float, de double ou d’un autre type, on devrait dupliquer la classe avec son code — peu pratique et source de bugs.
Solution : un template de classe, qui paramètre le type des cases. La même classe pourra alors générer des versions spécialisées pour int, float, double, etc.
Travail à réaliser¶
Créez une nouvelle classe DamierDynG<G>, version paramétrée de DamierDyn. La signature attendue :
template <class G>
class DamierDynG {
public:
DamierDynG(int l, int c, G val = G());
DamierDynG(const DamierDynG<G>& D);
~DamierDynG();
DamierDynG<G>& operator= (const DamierDynG<G>& D);
DamierDynG<G>& operator+= (const DamierDynG<G>& D);
DamierDynG<G>& operator+= (G c);
DamierDynG<G> operator+ (const DamierDynG<G>& D) const;
void Init(G val = G());
void Set(int x, int y, G val);
G Get(int x, int y) const;
void Print() const;
template <class G2>
friend std::ostream& operator<< (std::ostream& os, const DamierDynG<G2>& D);
private:
int L;
int C;
G** T;
void Alloc(int l, int c);
void Free();
bool sameDimensions(const DamierDynG<G>& D) const;
};
Quelques explications sur la syntaxe :
template <class G>introduit le paramètre de type.class Gettypename Gsont synonymes ; les deux écritures sont valides.- À l’intérieur de la classe,
Gse manipule comme un type concret (G val,G** T, etc.). G()désigne la valeur par défaut du typeG. Pourint, c’est0. Pourfloat, c’est0.0f. Pourbool, c’estfalse. Pour un objet utilisateur, c’est l’objet construit avec son constructeur sans argument.- L’écriture du
friend operator<<est plus complexe : il faut redéclarer un nouveau paramètre template (G2) pour la fonction libre. C’est un détail technique qu’on accepte sans s’attarder.
Particularité technique : code dans le .h¶
Le compilateur doit avoir accès au code complet d’un template au moment où il l’instancie (DamierDynG<int>, DamierDynG<float>, etc.). On ne peut donc pas séparer la déclaration et l’implémentation comme avec DamierDyn au BE #2.
→ Tout le code de DamierDynG doit être placé dans le fichier .h : déclaration de la classe et définition des méthodes.
L’implémentation des méthodes hors de la classe utilise la syntaxe suivante :
template <class G>
DamierDynG<G>::DamierDynG(int l, int c, G val) {
Alloc(l, c);
Init(val);
}
template <class G>
void DamierDynG<G>::Init(G val) {
for (int i = 0; i < L; ++i)
for (int j = 0; j < C; ++j)
T[i][j] = val;
}
Notez qu’il faut :
- répéter
template <class G>avant chaque définition ; - préfixer le nom de la classe par
DamierDynG<G>::(pas seulementDamierDynG::).
Erreur typique : si vous séparez quand même la déclaration (
.h) et l’implémentation (.cpp), le code compile mais l’éditeur de liens (linker) se plaint d’uneundefined referencedès que vous instanciez le template. Solution : tout mettre dans le.h.
Programme de test (Partie 2)¶
Voici un test à faire fonctionner :
#include "damierdyng.h"
int main() {
DamierDynG<float> Df(2, 2);
Df.Init(-1.4f);
Df += 4.5f;
Df.Set(1, 0, 13.4f);
std::cout << Df;
DamierDynG<int> Di(3, 3, 7);
Di += 2;
std::cout << Di;
DamierDynG<double> Dd(2, 3, 1.5);
DamierDynG<double> Dd2(Dd); // ctor de recopie
Dd2 += Dd;
std::cout << Dd2;
return 0;
}
Points à noter¶
template <class G>ettemplate <typename G>sont équivalents. Choisir un style et s’y tenir.- Un template ne génère du code machine que lorsqu’il est instancié.
DamierDynG<int>etDamierDynG<float>produisent deux classes distinctes en mémoire. - Si
Gest un type qui ne supporte pas l’opérateur utilisé (par exemple,G = std::stringne supporte pasT[i][j] += c), la compilation échouera au moment de l’instanciation. Le message d’erreur peut être déroutant.
Questions à se poser¶
- Pourquoi le code complet d’un template doit-il être placé dans le
.het non dans un.cpp? - Que se passe-t-il si
Gne dispose pas d’un constructeur par défaut (cas oùG()n’est pas valide) ? - Combien de versions de
DamierDynGse retrouvent dans l’exécutable si on utiliseDamierDynG<int>,DamierDynG<float>etDamierDynG<double>dans le même programme ?
Aller plus loin — vers les exceptions (BE #4)¶
Vous avez constaté que la gestion d’erreur sur dimensions incompatibles repose sur std::cerr + std::exit(1). Cette solution est brutale : std::exit(1) termine le programme sans appeler les destructeurs des objets en cours, ce qui provoque potentiellement des fuites de ressources (mémoire, fichiers ouverts, connexions…).
Au BE #4, vous remplacerez ces exit(1) par des exceptions, mécanisme propre du C++ qui :
- signale une erreur en remontant la pile d’appels jusqu’à un gestionnaire ;
- garantit que tous les destructeurs des objets locaux sont bien appelés en chemin (stack unwinding) — donc pas de fuite ;
- permet à l’appelant de réagir (réessayer, afficher, abandonner…) plutôt que d’imposer la fin du programme.