Polymorphisme (partie 2) et conversion d'objets
Méthode virtuelle pureClasse abstraite, interfaceCollection hétérogèneDécoupage (slicing)Conversion ascendante et descendantestatic_cast / dynamic_castdynamic_pointer_cast
1. Méthodes virtuelles pures et classes abstraites
Une méthode virtuelle pure est déclarée avec = 0 : la classe de base ne fournit aucune implémentation, elle impose seulement que chaque classe dérivée concrète en fournisse une. Une classe qui possède au moins une méthode virtuelle pure est abstraite : on ne peut pas créer d'objet de ce type (erreur « impossible d'instancier une classe abstraite »), mais on peut déclarer des pointeurs et des références vers elle, et c'est tout ce dont le polymorphisme a besoin.
- Une dérivée qui ne redéfinit pas toutes les méthodes pures reste abstraite (par exemple
SourceEnergieau TP3, qui ajoute sa propre méthode pureconsommeret laisse Batterie, PanneauSolaire et Cable la définir). - Une méthode pure peut quand même avoir un corps (défini hors classe), que les dérivées appellent avec
Base::methode(); c'est rare. - Une interface est une classe abstraite qui ne contient que des méthodes virtuelles pures (et un destructeur virtuel) : elle décrit un contrat, sans données. Exemple du simulateur de hockey :
MembreEquipeavecgetSalaire()etgetEquipe(). - Le destructeur virtuel d'une classe abstraite est obligatoire dès qu'on détruit via un pointeur de base, ce qui est le cas général.
« erreur C2259 : impossible d'instancier une classe abstraite » apparaît quand (1) on déclare un objet d'une classe abstraite par valeur, y compris vector<Composante> ou un paramètre Composante c ; ou (2) une classe dérivée a oublié de redéfinir une méthode pure, ou l'a redéfinie avec une signature différente (un const manquant), ce qui en fait une autre méthode. override détecte le second cas.
Collection hétérogène et traitement polymorphe (TP3)
L'énoncé du TP3 insiste : « le robot doit obtenir les affichages spécialisés par polymorphisme, sans tester chaque type de capteur ». Le test de type (dynamic_cast, typeid) est réservé aux cas où l'on a vraiment besoin d'une opération propre à une dérivée (recharger une Batterie, pas une source quelconque).
2. Découpage (slicing) : le polymorphisme a besoin d'un pointeur ou d'une référence
Passer un objet polymorphe par valeur à une fonction (void f(Composante c)) découpe aussi : toujours const Composante& ou un pointeur.
3. Conversions entre classes de base et dérivées
| Conversion | Direction | Légalité | Syntaxe |
|---|---|---|---|
| Ascendante (upcast) | dérivée → base (objet, pointeur, référence, shared_ptr) | toujours légale, implicite : un Moteur est une Composante | Composante* p = &moteur; shared_ptr<Composante> s = make_shared<Moteur>(...); |
| Descendante (downcast) | base → dérivée | jamais implicite ; légale seulement si l'objet est réellement de la classe dérivée | static_cast<Moteur*>(p) (sans vérification), dynamic_cast<Moteur*>(p) (vérifiée à l'exécution) |
| Objet base → objet dérivé | par valeur | erreur de compilation, sauf si la dérivée définit un constructeur prenant une base (Moteur(const Composante&)) | Moteur m = c; // erreur |
static_cast : conversion à la compilation
static_cast fait confiance au programmeur : il ne vérifie rien à l'exécution. À utiliser quand on sait que l'objet est du bon type (par exemple juste après l'avoir rangé dans un vecteur de pointeurs de base). Il sert aussi aux conversions numériques : static_cast<int>(3.7).
dynamic_cast : conversion vérifiée à l'exécution
- Exige que la classe de base soit polymorphe (au moins une méthode virtuelle, en pratique le destructeur virtuel) : le type réel est lu dans l'objet (RTTI).
- Résultat : le pointeur converti, ou
nullptr; avec une référence, exceptionstd::bad_cast. typeid(*p) == typeid(Batterie)teste le type exact (faux pour une dérivée de Batterie), alors quedynamic_castréussit aussi pour les dérivées.typeid(*p).name()donne un nom dépendant du compilateur.- Ne jamais reconstruire un pointeur intelligent à partir du pointeur brut obtenu par
dynamic_cast<T*>(sp.get())(double propriété). Utiliserdynamic_pointer_cast.
Le document du simulateur de hockey le dit sans détour : utiliser typeid et dynamic_cast pour distinguer les types est « une très mauvaise pratique » quand une méthode virtuelle ferait le travail. Si vous écrivez une chaîne de if (dynamic_cast<A*>(p)) ... else if (dynamic_cast<B*>(p)) ..., demandez-vous si une méthode virtuelle agir() dans la base ne serait pas la bonne solution. Réservez le downcast aux opérations qui n'ont de sens que pour une dérivée précise.
4. À retenir
virtual ... = 0;: méthode pure ; classe abstraite = au moins une pure ; non instanciable, mais pointeurs et références autorisés. Une dérivée qui ne définit pas toutes les pures reste abstraite. Interface = que des pures.- Polymorphisme = collection de pointeurs (bruts ou intelligents) vers la base + méthodes virtuelles. La copie par valeur découpe l'objet.
- Upcast implicite et toujours valide ; downcast jamais implicite :
static_cast(sans vérification, comportement indéfini si faux) oudynamic_cast(nullptr ou bad_cast si faux, base polymorphe requise) ;dynamic_pointer_castpour shared_ptr. - Préférer une méthode virtuelle à un test de type.
Quiz rapide - Module 6
Qu'est-ce qu'une classe abstraite ?
- Une classe sans attributs
- Une classe dont toutes les méthodes sont virtuelles
- Une classe qui possède au moins une méthode virtuelle pure ; on ne peut pas l'instancier
- Une classe sans constructeur
Comment déclare-t-on une méthode virtuelle pure ?
virtual void f() const {}virtual void f() const = 0;void f() const = delete;abstract void f() const;
= 0 signifie « pas d'implémentation ici, obligatoire dans les dérivées concrètes ». abstract n'existe pas en C++.Composante est abstraite. Laquelle de ces lignes compile ?
Composante c("x", 1);vector<Composante> v;puisv.push_back(Moteur(...));void f(Composante c);appelée avec un Moteurshared_ptr<Composante> p = make_shared<Moteur>("m", 1, 10);
Une classe dérivée d'une classe abstraite ne redéfinit qu'une des deux méthodes pures. Que se passe-t-il ?
- Elle est elle-même abstraite : on ne peut pas l'instancier
- Erreur de compilation à sa déclaration
- La méthode manquante est générée vide
- Elle compile et la méthode manquante lance une exception
Qu'affiche ce code ?
- wouf wouf
- ... ...
- ... wouf
- Erreur de compilation
a est une copie découpée : un pur Animal. r est une référence vers le Chien : liaison dynamique.Quelle conversion est toujours légale et implicite ?
- Base* → Dérivée*
- Dérivée* → Base*
- Base → Dérivée (par valeur)
- Aucune conversion n'est implicite entre classes
Que retourne dynamic_cast<Chien*>(p) si p (de type Animal*) pointe un simple Animal ?
- Un pointeur vers un Chien vide
- Le même pointeur, réinterprété
- Une exception
- nullptr
Et static_cast<Chien*>(p) dans la même situation ?
- Compile et retourne un pointeur, mais l'utiliser est un comportement indéfini
- nullptr
- Erreur de compilation
- Exception bad_cast
Quelle condition la classe de base doit-elle remplir pour que dynamic_cast fonctionne ?
- Avoir un constructeur par défaut
- Être abstraite
- Être polymorphe : avoir au moins une méthode virtuelle (typiquement le destructeur)
- Ne pas avoir d'attributs privés
Pour convertir un shared_ptr<Composante> en shared_ptr<Batterie> avec vérification, on utilise :
shared_ptr<Batterie>(dynamic_cast<Batterie*>(p.get()))dynamic_pointer_cast<Batterie>(p)static_cast<shared_ptr<Batterie>>(p)(shared_ptr<Batterie>) p
Quand un downcast est-il justifié plutôt qu'une méthode virtuelle ?
- Toujours : c'est plus rapide
- Jamais
- Quand la base est abstraite
- Quand l'opération n'a de sens que pour une dérivée précise (recharger une Batterie, pas une SourceEnergie quelconque)
Qu'est-ce qu'une interface en C++ ?
- Une classe abstraite composée uniquement de méthodes virtuelles pures (et d'un destructeur virtuel)
- Un fichier .h
- Une classe sans méthodes
- Un mot-clé du langage
Quelle différence entre typeid(*p) == typeid(Batterie) et dynamic_cast<Batterie*>(p) != nullptr ?
- Aucune
- typeid fonctionne sans méthodes virtuelles, dynamic_cast non ; les résultats sont identiques
- typeid teste le type exact ; dynamic_cast accepte aussi les classes dérivées de Batterie
- dynamic_cast teste le type exact ; typeid accepte les dérivées
Pourquoi operator<< du TP3 est-il écrit une seule fois pour Composante en déléguant à afficher ?
- Parce qu'on ne peut pas surcharger << pour une dérivée
- Parce que
afficherest virtuelle :cout << *paffiche alors selon le type réel, sans un opérateur par classe - Pour économiser de la mémoire
- Parce que << est une fonction membre
Qu'affiche ce code ?
- ok
- Erreur de compilation : B n'a pas de méthode virtuelle
- Comportement indéfini
- nul
Exercices à développement
SourceEnergie, Batterie, PanneauSolaire, Cable
SourceEnergie hérite de Composante (abstraite) et reste abstraite : elle fournit une tension (strictement positive), et déclare les méthodes virtuelles pures getCourantMaxA(), getCapaciteTotaleWh(), getCapaciteRestanteWh() et consommer(double energieWh) (retourne bool). Sa consommation propre est nulle. Elle offre getPuissanceMaxW() = tension × courant max et getTempsRestant(puissanceW) : 0 si la puissance est négative ou dépasse le maximum, infini si elle est nulle, sinon énergie restante / puissance.
Écrivez SourceEnergie, puis Batterie (capacité et courant max finis strictement positifs, commence pleine, consommer refuse si l'énergie manque, recharger plafonne à la capacité) et Cable (alimentation réseau : énergie illimitée, courant max donné).
Solution
Remarque : getPuissanceMaxW est définie dans la base mais appelle une méthode pure : à l'exécution, c'est la version de l'objet réel (Batterie ou Cable) qui répond. Pour le panneau solaire : puissance disponible = nominale × ensoleillement, courant max = puissance / tension, capacité infinie si éclairé, énergie nulle dans le noir (une demande positive est alors refusée).
Valide, erreur de compilation ou comportement indéfini ?
Equipement est une classe de base polymorphe ; Arme en hérite publiquement. Pour chaque ligne, dites si elle est valide, si elle ne compile pas, ou si elle compile mais a un comportement indéfini.
Solution
- Valide : conversion ascendante par copie (découpage).
- Valide : même chose, cast explicite inutile.
- Valide : upcast de pointeur.
- Erreur de compilation : pas de conversion Equipement → Arme, sauf constructeur
Arme(const Equipement&). - Erreur de compilation, même raison.
- Compile, mais
equipementest un simple Equipement : utiliserepeeptrest un comportement indéfini. - Valide : upcast.
- Valide :
equipptrpointe réellement une Arme (ligne 7), le downcast est correct. - Compile ; retourne
nullptrcar l'objet n'est pas une Arme (Equipement doit être polymorphe).
Remplacer les tests de type par du polymorphisme
Ce code fonctionne mais est mal conçu. Réécrivez-le avec une méthode virtuelle pour supprimer les dynamic_cast.
Solution
Chaque classe connaît sa propre règle de consommation ; ajouter un type de pièce ne demande plus de modifier la fonction. Les pièces sans consommation (Chassis, sources) retournent 0.
Devinez l'affichage
L'interface MembreEquipe
Définissez l'interface MembreEquipe (méthodes getSalaire() et getEquipe()). Joueur (déjà dérivée de Patineur) et Entraineur (nom, expérience, équipe, salaire) doivent l'implémenter. Puis écrivez bool Match::valider() const qui vérifie que la masse salariale de chaque équipe (true / false) ne dépasse pas 10 000 000, à partir d'un vector<shared_ptr<MembreEquipe>> membres_.