Polymorphism ************ .. include:: ../BoutonGoogleTrad.rst Visibilité et encapsulation =========================== Les :cpp:`structures` nous ont permis de travailler sans avoir à gérer les problèmes de visibilité car tous les membres d'instance étaient publics par défaut. Le **principe d'encapsulation** (:cppterm:`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 : :cppterm:`protected` * de rendre publiques les membres qui servent d'interface vers l'extérieur :cppterm:`public` Les :cpp:`structures` nous ont permis jusqu’ici de travailler sans nous préoccuper de la visibilité des membres, puisque ceux-ci sont publics par défaut. Le principe d` **encapsulation** (:cppterm:`encapsulation`) consiste au contraire à masquer les détails internes d'un objet et à ne rendre accessible depuis l'extérieur que ce qui est nécessaire à son utilisation. Certaines données peuvent en effet être sensibles : une modification incorrecte pourrait placer l'objet dans un état incohérent ou provoquer un dysfonctionnement. Il est donc généralement conseillé : * de rendre privées (:cppterm:`private`) les données et les fonctions destinées au fonctionnement interne de l'objet * de rendre publiques (:cppterm:`public`) les membres constituant l'interface accessible depuis l'extérieur Dans le cadre de l'héritage, les membres **privés** (:cpp:`private`) d'une classe de base ne sont pas accessibles depuis les classes dérivées. Le mot-clé :cppterm:`protected` permet de rendre ces membres inaccessibles depuis l'extérieur, tout en les laissant accessibles aux classes dérivées. .. code-block:: cpp #include using namespace std; class Character { protected : int _stamina = 100; // stamina bool CheckAndConsume(int amount) { if ( _stamina < amount ) return false; _stamina -= amount; return true; } public : void bash() // powerful attack { if ( CheckAndConsume(25) ) return; ... } void jump() { if ( CheckAndConsume(10) ) return; ... } int getStamina() { return _stamina; } void setStamina(int v) { if (v < 0) return; _stamina = v; } }; La fonction :cpp:`CheckAndConsume()` vérifie que le personnage possède suffisamment d’endurance avant d’en consommer. Comme elle sert uniquement au fonctionnement interne de la classe, elle n’a pas vocation à être appelée depuis l’extérieur. Elle est donc déclarée en :cpp:`protected`, au même titre que l’attribut :cpp:`_stamina`. Les autres membres sont déclarés en :cpp:`public` car ils constituent l’interface de la classe. Ils représentent les actions et les informations que le reste du programme est autorisé à utiliser. Héritage ======== L'héritage en C++ est un mécanisme fondamental de la programmation orientée objet (POO). Il permet de définir de nouvelles classes, appelées **classes dérivées** (:cppterm:`derived classes`), à partir de classes existantes appelées **classes de base** (:cppterm:`base classes`). Une classe dérivée peut ainsi réutiliser et spécialiser les caractéristiques et les comportements définis par sa classe de base. L'héritage favorise la réutilisation du code et permet de structurer plus clairement les 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 :cpp:`Derived` dispose de sa propre fonction :cpp:`fnt2()`, mais elle hérite aussi de la fonction :cpp:`fnt1()` de sa classe mère. Chaînage des constructeurs ========================== Principe -------- Lorsqu’une classe dérivée est instanciée, son constructeur est appelé comme habituellement. Cependant, cet objet contient des données provenant de la classe de base qui doivent aussi être correctement initialisée. Pour cela, il faut donc qu'un constructeur de la classe de base soit appelé. Voici comment cela fonctionne : .. panels:: :column: col-lg-10 p-2 Règles concernant le chaînage des constructeurs (:cppterm:`constructor chaining`) : * Si le constructeur par défaut de la classe de base suffit, il est appelé **implicitement**. * Si un constructeur paramétré de la classe de base doit être utilisé, le constructeur de la classe dérivée doit l’appeler **explicitement**. 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; } }; int main() { Dog d("Rex", "German Shepherd"); } Il est aussi possible d'utiliser la syntaxe suivante : .. code-block:: Animal(string n) : _name(n) { } Dog(string name, string breed) : Animal(name), _breed(breed) { } // beware of the order - chaining first Les erreurs ----------- Il est possible de ne pas chaîner et décrire l'initialisation des données membres de la classe de base dans le contructeur de la classe dérivée : .. code-block:: cpp Dog(string name, string breed) { _name = name; _breed = breed; } Certes, cela fonctionne, mais c'est une mauvaise pratique car une classe dérivée ne doit pas modifier des données internes de sa classe de base directement. En effet, le constructeur de la classe de base peut effectuer des vérifications, des conversions ou des initialisations supplémentaires qu’une affectation directe pourrait manquer. Le passage en :cpp:`private` des données de la classe de base permet d'éviter cet écueil. Polymorphisme d'héritage ======================== Présentation ------------ Dans une hiérarchie d'héritage, il est possible de mettre en place un **polymorphisme** :cppterm:`polymorphism`. Ce mécanisme permet à une fonction portant le même nom 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 dans la classe de base doit être qualifiée de :cppterm:`virtual`. | | Les fonctions redéfinies (:cppterm:`overridden`) dans les classes dérivées doivent utiliser le mot clef :cppterm:`override`. Par exemple, dans une hiérarchie d'objets géométriques : :cpp:`Triangle`, :cpp:`Circle` et :cpp:`Rectangle`. Il serait maladroit de créer des fonctions :cpp:`drawTriangle()`, :cpp:`drawCircle()` et :cpp:`drawRectangle()` pour afficher ces objets. Il est plus simple et plus lisible d'avoir une seule méthode :cpp:`draw()` pour toutes les classes et que cette méthode s'adapte suivant la nature de l'objet : .. code-block:: cpp #include #include using namespace std; struct GraphicObject { virtual void draw() { cout << "GraphicObject" << endl; } }; struct Triangle : public GraphicObject { void draw() override { cout << "Triangle" << endl; } }; struct Circle : public GraphicObject { void draw() override { cout << "Circle" << endl; } }; int main() { GraphicObject G; Triangle T; Circle C; G.draw(); >> GraphicObject T.draw(); >> Triangle C.draw(); >> Circle } Le début des problèmes ====================== Si nous voulons manipuler différents objets géométriques, il serait pratique de pouvoir stocker tous nos cercles et rectangles dans un seul :cpp:`vector`. Or, le template :cpp:`vector` nécessite de préciser le type des éléments qu’il contient. Il semble donc naturel d’utiliser le type de la classe mère : :cpp:`vector`. Voyons maintenant si cette approche permet de conserver le polymorphisme. .. code-block:: cpp #include #include using namespace std; struct GraphicObject { virtual void draw() { cout << "GraphicObject" << endl; } }; struct Triangle : public GraphicObject { void draw() override { cout << "Triangle" << endl; } }; struct Circle : public GraphicObject { void draw() override { cout << "Circle" << endl; } }; int main() { vector L; Triangle T; Circle C; L.push_back(T); L.push_back(C); L[0].draw(); L[1].draw(); // GraphicObject GraphicObject } Étrangement, tout compile correctement et le programme s’exécute, mais le polymorphisme ne fonctionne pas comme prévu. Quel que soit l’objet inséré dans le :cpp:`vector`, tout se passe comme s’il était uniquement du type de la classe mère :cpp:`ObjGraphique`. Pour comprendre ce phénomène, il faut se rappeler qu’un :cpp:`vector` stocke réellement des objets de type :cpp:`T`. Ainsi, dans : .. code-block:: cpp vector L; Tous les éléments contenus dans ce :cpp:`vector` sont des objets de type :cpp:`GraphicObject`. Que se passe-t-il alors lorsque l’on écrit : .. code-block:: cpp L.push_back( Circle(...) ); L'objet :cpp:`Circle` être converti en :cpp:`ObjGraphique` afin de pouvoir être stocké dans le vector. Seule la partie de l’objet correspondant à la classe mère est alors copiée : les données propres à l'objet :cpp:`Circle` sont perdues. .. panels:: :column: col-lg-10 p-2 **Object slicing** : Si :cpp:`B` est une classe dérivée de :cpp:`A` et que l'on stocke un objet :cpp:`B` dans un :cpp:`vector` alors l'objet est converti vers le type :cpp:`A`. Ce phénomène est appelé :cppterm:`object slicing`. Il empêche le polymorphisme de fonctionner correctement. Autrement dit, une fois placé dans un :cpp:`vector`, le cercle n’est plus stocké comme un véritable objet :cpp:`Circle`, mais comme un simple :cpp:`ObjGraphique`. Le polymorphisme ne peut donc plus fonctionner comme attendu. Ce mécanisme est identique à celui que l'on connait avec les types fondamentaux. Si vous stockez un :cpp:`double` dans un :cpp:`vector`, sa valeur sera tronquée et convertie en entier. Polymorphisme et vector ======================= La solution consiste à utiliser un :cpp:`vector` de :cpp:`shared` pointeurs. Dans ce cas précis, le langage vérifie dynamiquement à l'exécution le type réel de l'objet et sait gérer le polymorphisme. Mise en place ------------- Voici un exemple complet : .. code-block:: cpp #include #include #include using namespace std; struct GraphicObject { virtual void draw() { cout << "GraphicObject" << endl; } }; struct Triangle : public GraphicObject { void draw() override { cout << "Triangle" << endl; } }; struct Circle : public GraphicObject { void draw() override { cout << "Circle" << endl; } }; int main() { vector> L; L.push_back(make_shared()); L.push_back(make_shared()); for (auto& obj : L) obj->draw(); } >> Triangle >> Circle Dans cet exemple : * Différents objets sont créés ainsi que leur :cpp:`shared_ptr` en utilisant la syntaxe :cpp:`make_shared<>`. * Les smart pointeurs sont stockés dans le :cpp:`vector L`. * La méthode polymorphe :cpp:`draw()` donne un affichage correct pour chaque objet, le polymorphisme fonctionne ! Une fois la fonction :cpp:`main()` terminée, les compteurs de chaque :cpp:`shared_ptr` tombe à zéro et les objets sont automatiquement détruits. Quizzz ====== .. quiz:: herit :title: Héritage & Polymorphisme * :quiz:`{"type":"TF","answer":"F"}` En C++, le chaînage des constructeurs est automatique. * :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":"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":"TF","answer":"T"}` Le chaînage des constructeurs a pour objectif d'initialiser correctement les membres des classes parents. * :quiz:`{"type":"FB","answer":"override"}` Quel mot clef faut-il utiliser pour redéfinir une fonction polymorphe ? * :quiz:`{"type":"FB","answer":"private"}` Pour masquer un membre de ses enfants, on utilise le mot clef ? * :quiz:`{"type":"FB","answer":"protected"}` Pour masquer un membre de l'extérieur mais pas de ses enfants, on utilise le mot clef ?