Module 6 - cours du 22 septembre (quiz 5)

Polymorphisme (partie 2) et conversion d'objets

Leçons Moodle « Polymorphisme », « Conversion d'objets », document « Conversion avec un pointeur brut et un shared_ptr » | Vidéo « Classe abstraite et fonction virtuelle pure » | Mis en pratique au TP3 (Composante et SourceEnergie abstraites, dynamic_cast dans rechargerBatterie, dynamic_pointer_cast dans filtrerParType)

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.

Erreur de compilation classique (FAQ du cours)

« 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

ConversionDirectionLégalitéSyntaxe
Ascendante (upcast)dérivée → base (objet, pointeur, référence, shared_ptr)toujours légale, implicite : un Moteur est une ComposanteComposante* p = &moteur; shared_ptr<Composante> s = make_shared<Moteur>(...);
Descendante (downcast)base → dérivéejamais implicite ; légale seulement si l'objet est réellement de la classe dérivéestatic_cast<Moteur*>(p) (sans vérification), dynamic_cast<Moteur*>(p) (vérifiée à l'exécution)
Objet base → objet dérivépar valeurerreur 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

Laboratoire de conversions

Pour chaque conversion entre Animal (base polymorphe) et Chien : compile ? résultat ? Choisissez et vérifiez.

Usage au TP3
Bonne pratique

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

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
Elle peut avoir des attributs, un constructeur (appelé par les dérivées) et des méthodes ordinaires. Seule l'instanciation directe est interdite.

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;
Le = 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; puis v.push_back(Moteur(...));
  • void f(Composante c); appelée avec un Moteur
  • shared_ptr<Composante> p = make_shared<Moteur>("m", 1, 10);
Objets par valeur interdits (a, b, c : instanciation ou copie d'une classe abstraite) ; pointeurs et références autorisés.

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
C'est le cas de SourceEnergie au TP3 : Composante abstraite → SourceEnergie encore abstraite → Batterie concrète.

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
Conversion ascendante : une Dérivée est une Base, le compilateur l'accepte sans cast.

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
Avec un pointeur, l'échec se signale par nullptr ; avec une référence, par l'exception std::bad_cast.

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
static_cast ne vérifie rien à l'exécution. C'est au programmeur de garantir le type réel.

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
Le type réel est stocké avec la table des méthodes virtuelles ; sans méthode virtuelle, il n'y a rien à consulter : erreur de compilation.

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
La première crée un second propriétaire indépendant du même objet : double destruction. dynamic_pointer_cast partage le compteur.

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)
Si toutes les dérivées peuvent répondre (afficher, consommer), c'est une méthode virtuelle. Si une seule le peut, dynamic_cast est acceptable.

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
Exemple : MembreEquipe avec getSalaire() et getEquipe() dans le simulateur de hockey. Joueur et Entraineur en héritent et l'implémentent.

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
Pour une classe BatterieLithium : Batterie, typeid répond faux, dynamic_cast réussit.

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 afficher est virtuelle : cout << *p affiche alors selon le type réel, sans un opérateur par classe
  • Pour économiser de la mémoire
  • Parce que << est une fonction membre
Un opérateur libre ne peut pas être virtuel ; il obtient le polymorphisme en appelant une méthode virtuelle.

Qu'affiche ce code ?

  • ok
  • Erreur de compilation : B n'a pas de méthode virtuelle
  • Comportement indéfini
  • nul
B est polymorphe grâce à son destructeur virtuel ; l'objet n'est pas un D : nullptr, donc « nul ».

Exercices à développement

TP3Classes abstraites

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).

Révision H2019Conversions

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
  1. Valide : conversion ascendante par copie (découpage).
  2. Valide : même chose, cast explicite inutile.
  3. Valide : upcast de pointeur.
  4. Erreur de compilation : pas de conversion Equipement → Arme, sauf constructeur Arme(const Equipement&).
  5. Erreur de compilation, même raison.
  6. Compile, mais equipement est un simple Equipement : utiliser epeeptr est un comportement indéfini.
  7. Valide : upcast.
  8. Valide : equipptr pointe réellement une Arme (ligne 7), le downcast est correct.
  9. Compile ; retourne nullptr car l'objet n'est pas une Arme (Equipement doit être polymorphe).
ConceptionPolymorphisme

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.

AffichageVirtuel pur et conversions

Devinez l'affichage

Vérifiez votre réponse

Explication

decrire n'est pas virtuelle mais appelle aire(), virtuelle pure : chaque objet répond avec sa version. Le dynamic_cast sur le pointeur brut d'observation (get()) réussit pour le Carre et vaut nullptr pour le Disque. On n'a pas créé de nouveau unique_ptr à partir de c : pas de double propriété.

Simulateur de hockeyInterface

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_.

Solution