Héritage multiple, fonctions et classes génériques
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).
- Ordre de construction : les classes de base dans l'ordre où elles apparaissent dans
class Joueur : public Patineur, public MembreEquipe(pas l'ordre de la liste d'initialisation), puis les attributs, puis le corps. Destruction inverse. - Ambiguïté : si deux bases ont un membre de même nom (
afficher()dans Patineur et dans MembreEquipe), l'appelj.afficher()est ambigu : erreur de compilation. On précise la portée :j.Patineur::afficher(), ou on redéfinitafficher()dans Joueur (qui peut appeler les deux versions). - La taille de l'objet est la somme des parties ; un pointeur vers la seconde base pointe à l'intérieur de l'objet (le compilateur ajuste l'adresse lors des conversions : ne jamais convertir avec un cast « brut » de type C).
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_.
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.
- Le type est déduit des arguments quand c'est possible ; sinon (type de retour seul, conversions) on le précise entre chevrons :
filtrerParType<Pince>(liste). - Les erreurs apparaissent à l'instanciation, souvent avec des messages longs : lisez la première ligne (« no match for operator< ») et la ligne « required from » qui indique l'appel fautif.
- La définition va dans le fichier .h : le compilateur doit voir le code complet pour l'instancier dans chaque
.cppqui l'utilise. C'est l'exception à la règle « déclaration dans le .h, définition dans le .cpp » (énoncé des TP : « les définitions des classes et fonctions génériques restent dans les en-têtes »). - Une fonction générique peut recevoir une fonction en paramètre (pointeur de fonction, foncteur ou lambda, module 9) :
sommer(liste, propriete)au TP3.
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>.
- Dans la classe,
Listeseul désigneListe<T>. Hors de la classe, une méthode définie séparément s'écrittemplate <typename T> int Liste<T>::chercher(const T& valeur) const { ... }, toujours dans le .h. - Les exigences sur T sont implicites :
Liste<T>ne compile que siTaoperator==. Pourshared_ptr,==compare les adresses (même objet), ce qui est exactement la règle de doublon du TP3 (« un doublon désigne le même objet, non deux objets distincts portant le même nom »). - Paramètres non-types :
template <typename T, int N> class Tableau { T donnees_[N]; };puisTableau<double, 3>. - Valeur par défaut :
template <typename T = int>. - Spécialisation : on peut fournir une version particulière pour un type donné (
template <> class Liste<bool>), hors programme sauf mention en cours.
4. À retenir
- Héritage multiple :
class D : public B1, public B2; bases construites dans l'ordre de la déclaration ; ambiguïté levée parB1::membre; diamant résolu parvirtual public(la classe la plus dérivée construit la base commune). Cas sain : une classe + des interfaces. template <typename T>devant une fonction ou une classe ; T déduit des arguments pour les fonctions, précisé entre chevrons pour les classes (Liste<int>) ou quand la déduction est impossible.- Les définitions génériques restent dans le .h ; les erreurs surgissent à l'instanciation ; T doit fournir les opérations utilisées.
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
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)
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
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
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
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
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
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)
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
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
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.hsize_t Liste::size() const { ... }dans Liste.cppsize_t Liste<T>::size() const { ... }dans Liste.cpp- Impossible : les méthodes génériques sont obligatoirement dans la classe
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)
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>
Exercices à développement
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").
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é.
Ordre de construction avec deux bases
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().
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).