Polymorphism ************ .. include:: ../BoutonGoogleTrad.rst Héritage ======== L'héritage en C++ est un concept fondamental de la programmation orientée objet (POO). Cette approche permet de créer de nouvelles classes appelées **classes dérivées** ou **classes filles** héritant des caractéristiques d'une classe pré-existante appelée **classe mère** ou **classe de base**. Cela permet une meilleure réutilisation du code et une structuration plus claire des classes appartenant à une même hiérarchie. Dans cet exemple, la classe fille hérite des fonctions de sa classe parent : .. code-block:: cpp #include using namespace std; struct Base { void fnt1() { cout << "fnt1" << endl; } }; struct Derived : Base { void fnt2() { cout << "fnt2" << endl; } }; int main() { Derived obj; obj.fnt1(); obj.fnt2(); } >> fnt1 >> fnt2 La classe *Derivee* dispose de sa propre fonction *fnt2()* et elle hérite aussi de la fonction *fnt1* de sa classe mère. Visibilité et encapsulation =========================== Les structs nous ont permis de travailler sans avoir à gérer les problèmes de visibilité car tous les membres d'instance étaient publics. Le **principe d'encapsulation** nous conseille de cacher les paramètres internes ainsi que le fonctionnement d'un objet de l'extérieur. En effet, certaines variables peuvent être plus sensibles que d'autres et si on venait à les modifier par mégarde, l'objet pourrait se mettre à dysfonctionner complètement. Ainsi, il est conseillé : * de rendre inaccessible depuis l’extérieur les données et les fonctions internes (protected) * de rendre publiques les membres qui servent d'interface vers l'extérieur (public) .. code-block:: cpp #include using namespace std; class Character { protected : int _stamina = 100; // stamina public : void bash() // powerful attack { if ( CheckAndConsume(25) ) return; ... } void jump() { if ( CheckAndConsume(10) ) return; ... } int getStamina() { return _stamina; } bool CheckAndConsume(int amout) { if ( _stamina < amount ) return false; _stamina -= amount; return true; } void setStamina(int v) { if (v < 0) return; _stamina = v; } }; Dans cet exemple, l’endurance d’un personnage diminue lorsqu’il court ou saute. Si elle devient insuffisante, le personnage ne peut plus attaquer ni sauter et doit récupérer. Pour garantir le bon fonctionnement du programme, cet attribut doit rester strictement positif : c’est précisément le rôle du setter *setStamina*. En effet, une endurance négative pourrait entraîner divers dysfonctionnements, notamment lors de l’affichage de la barre de stamina à l’écran. L’attribut *_stamina* constitue donc un paramètre interne ; il doit être rendu inaccessible depuis l’extérieur et est déclaré en privé. .. note:: Il existe un mot clef supplémentaire : **private** qui permet de rendre privé un membre relativement aux classes dérivées cette fois. Le mot clef **protected** laisse visible les membres depuis les classes enfants. Chaînage des constructeurs ========================== Principe -------- Lorsque qu'une classe fille est instanciée, son constructeur est appelé. Cependant, une partie de ce nouvel objet provient de la classe mère et pour fonctionner correctement, la partie de l'objet correspondant au parent doit être initialisé par le constructeur du parent spécifiquement. Il n'existe pas de mécanisme gérant automatiquement cela et **vous devez chaîner explicitement l'appel vers le constructeur parent depuis le constructeur de l'enfant**. Voici la syntaxe associée : .. code-block:: cpp #include using namespace std; class Animal { protected : string _name; public : Animal(string n) { _name = n; } }; class Dog : public Animal { protected : string _breed; public : Dog(string name, string breed) : Animal(name) // constructor chaining { _breed = breed; } void Info() { cout << "Name: " << _name << " Breed: " << _breed << endl; } }; int main() { Dog d("Rex", "German Shepherd"); d.Info(); } Il existe une autre syntaxe que vous pourrez rencontrer : .. code-block:: Animal(string n) { _name = n; } <====> Animal(string n) : _nom(n) { } Dog(string name, string breed) : Animal(name) { _breed = breed; } <=====> Dog(string name, string breed) : Animal(name), _breed(breed) { } // beware of the order - Animal ctr first Les erreurs ----------- Il est possible que pour contourner le chaînage, vous écriviez finalement le code suivant : .. code-block:: cpp Dog(string name, string breed) { _name = name; _breed = breed; } Dans cette version, le constructeur du parent n’est plus appelé : les données du parent sont directement initialisées dans le constructeur de l’enfant. Certes, cela fonctionne dans ce petit exemple, mais ce n’est pas une bonne pratique et il faut l’éviter. En effet, si le constructeur de la classe parente contenait des traitements supplémentaires — par exemple l’initialisation d’un conteneur de données —, alors en le contournant, l’objet *Chien* risquerait de ne pas fonctionner correctement. Pour éviter cette situation, le concepteur doit déclarer l’attribut *_nom* de la classe *Animal* en private, afin d’éviter ce genre de problème. Polymorphisme ============= Présentation ------------ Dans une hiérarchie d'héritage, il est possible de mettre en place un **polymorphisme d'héritage**. Ce mécanisme permet à une fonction portant le même dans diverses classes de la hiérarchie d'avoir un comportement différent suivant la classe considérée. Pour cela on utilise les mots-clefs suivants : .. panels:: :column: col-lg-10 p-2 | La fonction en haut de la hiérarchie doit être qualifiée de **virtual**. | | Les fonctions surchargées (redéfinies) dans les classes filles doivent utiliser le mot clef **override**. Par exemple, dans une hiérarchie d'objets géométriques (Triangle Cercle Rectangle), il serait maladroit de créer des fonctions *AfficheTriangle*, *AfficheCercle* et *AfficheRectangle* pour demander l'affichage de ces objets. Il est plus simple et plus lisible d'avoir une seule méthode *Affiche* pour toutes ces classes et que cette méthode varie suivant la nature de l'objet : .. code-block:: cpp #include #include using namespace std; struct GraphicObject { virtual void display() { cout << "GraphicObject" << endl; } }; struct Triangle : public GraphicObject { void display() override { cout << "Triangle" << endl; } }; struct Circle : public GraphicObject { void display() override { cout << "Circle" << endl; } }; Le début des problèmes ---------------------- Testons le code suivant pour voir si le polymorphisme fonctionne : .. code-block:: cpp #include #include using namespace std; struct GraphicObject { virtual void display() { cout << "GraphicObject" << endl; } }; struct Triangle : public GraphicObject { void display() override { cout << "Triangle" << endl; } }; struct Circle : public GraphicObject { void display() override { cout << "Circle" << endl; } }; int main() { GraphicObject O; Triangle T; Circle C; O.display(); T.display(); C.display(); // GraphicObject Triangle Circle //////////////////////////////////////////////////////////// GraphicObject A[3]; A[0] = O; A[1] = T; A[2] = C; // slicing occurs here A[0].display(); A[1].display(); A[2].display(); // GraphicObject GraphicObject GraphicObject //////////////////////////////////////////////////////////// vector L; // by-value => slicing again L.push_back(O); L.push_back(T); // sliced to GraphicObject L.push_back(C); // sliced to GraphicObject L[0].display(); L[1].display(); L[2].display(); // GraphicObject GraphicObject GraphicObject } Étrangement tout compile correctement, le programme se lance et pourtant rien ne fonctionne correctement ! Quel que soit l'objet instancié, tout se passe comme s'ils étaient du type de la classe mère : *ObjGraphique*. Pour comprendre cette situation, il faut se rappeler comment est construit le langage C++. En effet, que ce soit pour un tableau ou un *vector*, les éléments sont stockés de manière contiguë en mémoire et ils doivent tous faire la même taille et donc tous être du même type. Ainsi, lorsque vous écrivez *ObjGraphique A[3];* ou *vector*, tous les objets contenus dans ces containers sont forcément du type *ObjGraphique*. Ainsi, lorsque l'on écrit : * A[1] = T; * L[1] = T; Comme la classe *Triangle* appartient à la hiérarchie des *ObjGraphique*, le compilateur **autorise** sa troncature/conversion vers le type *ObjGraphique*, un peu comme lorsque l'on écrit : * int a = 2.14; Ainsi, l'objet *Triangle* et l'objet *Cercle* sont copiés et découpés/convertis pour être stockés sous forme d\'*ObjGraphiques*. Ce mécanisme est bien évidemment irréversible. Faire fonctionner le polymorphisme ================================== Résumé des contraintes ---------------------- Supposons que l'on dispose d'une hiérarchie d'objets dont la classe mère est *T*. Si l'on veut stocker plusieurs objets d'une même hiérarchie dans un même container, on ne peut utiliser un *vector* comme on vient de le voir. Le langage C++ ne garantit le fonctionnement du polymorphisme que pour les pointeurs. Mais, apparaît alors le problème de la destruction des objets. En effet, si l'on détruit l'objet *vector*, on ne détruit pas les objets liés à ses pointeurs, mais juste les pointeurs ! La solution consiste donc à combiner toutes les nouveautés vues jusqu'ici : * Utiliser des objets *vector* comme container. * Utiliser des *shared_ptr* pour s'assurer que les objets soient libérés une fois qu'ils ne sont plus utilisés. * Mettre en place le polymorphisme d'héritage pour permettre à chaque objet de la liste d'agir suivant sa nature. Mise en place ------------- Voici un exemple complet : .. code-block:: cpp #include #include #include using namespace std; struct GraphicObject { GraphicObject() { cout << "Ctor GraphicObject" << endl; } virtual void display() { cout << "GraphicObject" << endl; } virtual ~GraphicObject() { cout << "Dest GraphicObject" << endl; } }; struct Triangle : public GraphicObject { Triangle() : GraphicObject() { cout << "Ctor Triangle" << endl; } void display() override { cout << "Triangle" << endl; } ~Triangle() override { cout << "Dest Triangle" << endl; } }; struct Circle : public GraphicObject { Circle() : GraphicObject() { cout << "Ctor Triangle" << endl; } void display() override { cout << "Circle" << endl; } ~Circle() override { cout << "Dest Circle" << endl; } }; int main() { vector> L; L.push_back(make_shared()); // Ctor GraphicObject - Ctor Triangle L.push_back(make_shared()); // Ctor GraphicObject - Ctor Circle L.push_back(make_shared()); // Ctor GraphicObject for (const auto& o : L) o->display(); // Triangle - Circle - GraphicObject // Dest Triangle - Dest GraphicObject // Dest Circle - Dest GraphicObject // Dest GraphicObject } Dans cet exemple, différents objets sont créés ainsi que leur shared pointers en utilisant la syntaxe *make_shared<>*. Les smart pointeurs sont stockés dans le *vector L*. L'affichage indique que pour l'objet Triangle et Circle, le constructeur de la classe parent est d'abord exécuté suivi du constructeur de la classe fille. La méthode polymorphe *affiche()* donne un affichage correct pour chaque objet, le polymorphisme fonctionne. Une fois le programme terminé, les compteurs des *shared_ptr* tombent à zéro et les objets sont automatiquement détruits. L'affichage indique que pour l'objet Triangle et Circle, le destructeur de l'objet enfant est d'abord exécuté et ensuite celui du parent. C'est l'ordre inverse des constructeurs. .. note:: En C++, les destructeurs doivent être déclarés comme virtuels dans une hiérarchie. Les destructeurs sont automatiquement chaînés, vous n'avez pas à gérer ce point. .. warning:: Pourquoi à partir d'une const référence, peut-on appeler la méthode *display()* connue pour être non const ? .. code-block:: cpp for (const auto& o : L) o->display(); La variable o est un *shared ptr* et c'est lui qui reçoit le qualificatif *const*, il est donc non modifiable, on ne peut changer ce smart pointeur. Il va toujours pointer vers le même objet. Cependant, cela n'implique rien pour l'objet pointé, on peut tout de même appeler des méthodes non const depuis ce pointeur const. Ce mécanisme peut être troublant car il est différent de celui des références. En effet, depuis une const référence, on ne peut appeler que les méthodes const de l'objet. Quizzz ====== .. quiz:: herit :title: Héritage & Polymorphisme * :quiz:`{"type":"TF","answer":"F"}` En C++, le chaînage des constructeurs est automatique. * :quiz:`{"type":"FB","answer":"private"}` Pour masquer un membre de ses enfants, on utilise le mot clef ? * :quiz:`{"type":"TF","answer":"F"}` Le polymorphisme consiste pour une méthode d'instance à pouvoir changer de définition durant l'exécution du programme. * :quiz:`{"type":"FB","answer":"virtual"}` Quel mot clef faut-il insérer pour un destructeur s'il s'insère dans une hiérarchie d'héritage ? * :quiz:`{"type":"TF","answer":"F"}` Le C++ gère automatiquement le système de chaînage. * :quiz:`{"type":"TF","answer":"T"}` En C++, la conversion d'un objet vers le type de sa classe mère est autorisée. * :quiz:`{"type":"FB","answer":"override"}` Quel mot clef faut-il utiliser pour redéfinir une fonction polymorphe ? * :quiz:`{"type":"TF","answer":"T"}` Le chaînage des constructeurs a pour objectif d'initialiser correctement les membres des classes parents. * :quiz:`{"type":"FB","answer":"protected"}` Pour masquer un membre de l'extérieur mais pas de ses enfants, on utilise le mot clef ?