Module 7 - cours du 6 octobre (quiz 6) - dernier module de l'intra

Héritage multiple, fonctions et classes génériques

Leçons Moodle « Héritage multiple », « Fonctions et classes génériques » | Mis en pratique au TP3 (Liste<T>, sommer, filtrerParType<Derivee>) et dans le simulateur de hockey (Joueur hérite de Patineur et de MembreEquipe)

Héritage multipleAmbiguïté et Base::membreDiamant et héritage virtueltemplate <typename T>Fonction génériqueClasse génériqueDéfinitions dans le .h

1. Héritage multiple

Une classe peut hériter de plusieurs classes de base : class Joueur : public Patineur, public MembreEquipe. Elle reçoit tous les membres de chacune, et un Joueur peut être converti implicitement vers l'une ou l'autre (Patineur* ou MembreEquipe*). Usage le plus sain : hériter d'une classe concrète et d'une ou plusieurs interfaces (classes abstraites sans données).

Le problème du diamant et l'héritage virtuel

Si Joueur hérite de Patineur et d'Employe, et que ces deux classes héritent de Personne, un Joueur contient deux sous-objets Personne (deux nom_) : j.getNom() est ambigu et les données sont dupliquées. Solution : déclarer l'héritage virtuel dans les classes intermédiaires, pour que la base commune ne soit présente qu'une fois.

Avec une base virtuelle, c'est la classe la plus dérivée qui appelle le constructeur de la base commune (les appels faits par Patineur et Employe sont ignorés). Sans virtual, il faudrait écrire j.Patineur::nom_.

Conseil du cours

L'héritage multiple de classes concrètes complique vite les choses (diamant, ambiguïtés, ordre de construction). Le cas d'usage courant et sans danger : une classe concrète + des interfaces. Quand deux bases concrètes partagent un ancêtre, pensez héritage virtuel, ou remplacez l'une des bases par une composition.

2. Fonctions génériques (templates)

Une fonction générique est un modèle de fonction paramétré par un ou plusieurs types. Le compilateur génère (instancie) une version de la fonction pour chaque type utilisé, en vérifiant que les opérations employées (<, +, ==...) existent pour ce type.

TP3 : sommer et filtrerParType

3. Classes génériques

Une classe générique est un modèle de classe : Liste<int>, Liste<string> et Liste<shared_ptr<Composante>> sont trois classes distinctes générées à partir du même code. C'est ainsi que sont écrits vector<T>, shared_ptr<T>, map<K, V>.

Devinez l'affichage : instanciation et exigences sur T

Explication

v_ + v_ est une addition pour int et une concaténation pour string : le même code générique, deux instanciations. plusGrand(string("x"), string("abc")) compare lexicographiquement : « x » > « abc ». La construction de c affiche « Boite(5) ». plusGrand(a, c) retourne une copie de c (3 < 5) faite par le constructeur de copie implicite, qui n'affiche rien ; puis c < c est faux. Si Boite n'avait pas operator<, plusGrand(a, c) ne compilerait pas, mais plusGrand(4, 9) si : l'erreur n'apparaît qu'à l'instanciation fautive.

4. À retenir

Quiz rapide - Module 7

Dans class Joueur : public Patineur, public MembreEquipe, dans quel ordre les bases sont-elles construites ?

  • Dans l'ordre de la liste d'initialisation du constructeur de Joueur
  • Patineur puis MembreEquipe : l'ordre de la déclaration de la classe
  • MembreEquipe puis Patineur
  • Par ordre alphabétique
Même règle que pour les attributs : l'ordre de déclaration, pas celui de la liste d'initialisation.

Patineur et MembreEquipe ont chacune une méthode afficher(). Que donne j.afficher() pour un Joueur qui n'a pas redéfini afficher ?

  • La version de Patineur (première base)
  • Les deux versions, l'une après l'autre
  • Erreur de compilation : appel ambigu ; il faut écrire j.Patineur::afficher() ou redéfinir afficher dans Joueur
  • La version de MembreEquipe (dernière base)
Le compilateur ne choisit jamais arbitrairement entre deux bases.

Quel problème l'héritage virtuel résout-il ?

  • La duplication de la base commune dans un diamant (deux sous-objets Personne dans un Joueur)
  • L'impossibilité d'hériter de deux classes
  • L'absence de destructeur virtuel
  • Le découpage des objets
Avec class Patineur : virtual public Personne et class Employe : virtual public Personne, un Joueur ne contient qu'une seule Personne.

Avec une base virtuelle commune, qui appelle le constructeur de cette base ?

  • La première classe intermédiaire
  • Chaque classe intermédiaire, une fois chacune
  • Personne : la base est construite par défaut obligatoirement
  • La classe la plus dérivée, dans sa liste d'initialisation
Les appels des classes intermédiaires sont ignorés quand elles ne sont pas la classe la plus dérivée.

Pourquoi la définition d'une fonction générique doit-elle être dans le fichier .h ?

  • Parce que le .cpp ne peut pas contenir de template
  • Parce que le compilateur doit voir le code complet pour générer une version par type utilisé dans chaque fichier qui l'appelle
  • Parce que les templates ne sont pas compilés
  • C'est une simple convention des TP
Une fonction générique définie dans un .cpp séparé donne « symbole externe non résolu » à l'édition des liens pour les types instanciés ailleurs.

Avec template <typename T> T maximum(const T& a, const T& b), l'appel maximum(3, 2.5) :

  • Retourne 3
  • Retourne 2.5
  • Ne compile pas : T est déduit int d'un côté et double de l'autre
  • Retourne 3.0
Il faut lever l'ambiguïté : maximum<double>(3, 2.5) ou maximum(3.0, 2.5).

Quand l'erreur « no match for operator< » d'une fonction générique apparaît-elle ?

  • À l'instanciation avec un type qui ne définit pas <
  • À la définition du template
  • À l'exécution
  • Jamais : le compilateur génère un operator< par défaut
Le modèle lui-même n'est vérifié que syntaxiquement ; les exigences sur T sont testées pour chaque type réellement utilisé.

Dans filtrerParType<Pince>(composantes_), pourquoi faut-il écrire <Pince> ?

  • Par convention
  • Parce que les fonctions génériques exigent toujours les chevrons
  • Pour accélérer la compilation
  • Parce que Derivee n'apparaît pas dans les paramètres : il ne peut pas être déduit (Base, lui, est déduit de la liste)
Le paramètre de type non déductible est placé en premier dans la liste du template pour pouvoir le préciser seul.

Liste<int> et Liste<string> sont :

  • Le même type avec des contenus différents
  • Deux classes distinctes générées à partir du même modèle
  • Deux objets de la classe Liste
  • Incompatibles avec vector
On ne peut pas affecter l'une à l'autre ni passer l'une à une fonction attendant l'autre.

Pour Liste<shared_ptr<Composante>>, que compare elements_[i] == valeur ?

  • Les noms des composantes
  • Les contenus des objets (operator== de Composante)
  • Les adresses : vrai si les deux shared_ptr pointent le même objet
  • Les compteurs de références
C'est la règle de doublon du TP3 : même objet, pas même nom.

Comment définir hors de la classe la méthode size() de Liste<T> ?

  • template <typename T> size_t Liste<T>::size() const { return elements_.size(); } dans Liste.h
  • size_t Liste::size() const { ... } dans Liste.cpp
  • size_t Liste<T>::size() const { ... } dans Liste.cpp
  • Impossible : les méthodes génériques sont obligatoirement dans la classe
Le préfixe template se répète et le nom de classe porte ses paramètres ; toujours dans l'en-tête.

Quel est le cas d'usage de l'héritage multiple recommandé par le cours ?

  • Hériter de plusieurs classes concrètes pour réutiliser leur code
  • Remplacer la composition
  • Créer des diamants
  • Une classe concrète qui implémente une ou plusieurs interfaces (classes abstraites sans données)
Pas de données dupliquées, pas d'ambiguïté : le contrat de chaque interface est honoré par la classe.

Dans sommer(liste, propriete) du TP3, le second paramètre est de type générique Fonction. Pourquoi ?

  • Pour accepter uniquement des pointeurs de fonction
  • Pour accepter n'importe quel objet appelable : fonction, foncteur ou lambda, chacun ayant son propre type
  • Parce que double n'est pas un type valide de template
  • Pour éviter d'inclure <functional>
Chaque lambda a un type unique ; un paramètre de type générique les accepte toutes (module 9).

Exercices à développement

Fonctions génériques

Trois fonctions génériques

Écrivez les fonctions génériques : (1) echanger(T& a, T& b) ; (2) compter(const vector<T>& v, const T& valeur) qui retourne le nombre d'occurrences ; (3) afficherTout(const vector<T>& v, ostream& out) qui écrit les éléments séparés par des espaces. Quelles opérations chaque fonction exige-t-elle de T ?

Solution

Piège de déduction : compter(noms, "Koivu") avec vector<string> ne compile pas (T déduit string et const char[6]) ; écrire compter(noms, string("Koivu")) ou compter<string>(noms, "Koivu").

Classe génériqueTP3

Pile<T>

Écrivez une classe générique Pile<T> (dernier entré, premier sorti) construite sur un vector<T>, avec empiler(const T&), depiler() (retire et retourne le sommet ; précondition : pile non vide), sommet() const, estVide() const, taille() const, et un operator<< ami qui affiche du sommet vers le fond. Montrez une utilisation avec Pile<string>.

Solution

La boucle descendante avec size_t évite le débordement de i >= 0 sur un entier non signé.

Héritage multipleAffichage

Ordre de construction avec deux bases

Devinez l'affichage

Explication

Les bases sont construites dans l'ordre de class C : public B, public A (B puis A), malgré la liste d'initialisation A(), B() (le compilateur avertit). Puis l'attribut x_, puis le corps. Destruction en ordre exactement inverse. Tout est sur une seule ligne, car aucun endl.

Héritage multipleConception

Diamant : Personne, Patineur, Employe, Joueur

Personne a un nom ; Patineur et Employe héritent de Personne ; Joueur hérite de Patineur et d'Employe. (1) Sans héritage virtuel, que contient un Joueur et pourquoi j.getNom() ne compile-t-il pas ? Comment l'appeler quand même ? (2) Corrigez la hiérarchie avec l'héritage virtuel et écrivez les quatre constructeurs.

Solution

(1) Deux sous-objets Personne (un via Patineur, un via Employe), donc deux nom_ et deux getNom() : l'appel est ambigu. Il faut préciser : j.Patineur::getNom() ou static_cast<Patineur&>(j).getNom().

TP3Générique + polymorphisme

getSourceEnergie et ajouterComposante

Avec Liste<shared_ptr<Composante>> composantes_ et la fonction filtrerParType du cours, écrivez shared_ptr<SourceEnergie> Robot::getSourceEnergie() const (la source ou nullptr) et bool Robot::ajouterComposante(const shared_ptr<Composante>& c) qui refuse un pointeur nul, un objet déjà présent, ou une seconde source d'énergie.

Solution

Deux objets distincts de même nom peuvent coexister : chercher compare les shared_ptr (adresses).