L'orienté objet
Le C++ permet de faire de la programmation orientée objet, de manière similaire aux langages Java ou C#, en définissant des classes polymorphiques.
Le runtime polymorphism permet d'utiliser des classes différentes héritant d'une classe commune de la même façon, sans connaître leur type exact : pour cela, on définit (ou redéfinit) des fonctions de la classe mère dans la classe fille, en utilisant des fonctions virtuelles :
L'héritage
Une classe hérite d'une autre classe avec cette notation : class Child : public Mother, protected Father { ... };
. On peut hériter de plusieurs classes à la fois, et restreindre la portée des fonctions de la classe mère avec public
(accessible de partout, portée par défaut pour les struct
), protected
(accessible dans les classes filles) et private
(uniquement accessible dans la classe courante. Il s'agit de la portée par défaut pour les class
).
L'autre méthode pour éviter le problème consiste à n'hériter que d'une classe n'étant pas une interface. Hériter plusieurs fois de la même interface ne posera pas de problèmes car aucune donnée n'est recopiée, et aucune fonction n'est implémentée.
En C++, une interface est une classe qui ne contient que des fonctions virtuelles pures (non définies) :
Les tables virtuelles
Dès qu'une classe a une fonction virtuelle, elle va avoir une table virtuelle contenant un pointeur vers chacune de ses fonctions virtuelles. Chaque instance de la classe va alors avoir un pointeur vers cette table : il s'agit d'une classe polymorphique.
Lorsqu'on créé une classe héritant de cette classe, une nouvelle table virtuelle est crée, contenant les fonctions de la classe mère, remplacées par celles définies par la classe fille.
L'appel à une fonction virtuelle d'un objet va donc :
Accéder au pointeur de la table virtuelle de la fonction stocké dans l'objet.
Lire la table virtuelle, à l'index où se trouve le pointeur de fonction de la 'vraie' classe de l'objet.
Appeler la fonction.
Ces indirections rendent les fonctions virtuelles impossible à inline pour le compilateur, si on ne possède pas l'objet avec son vrai type. Marquer une redéfinition de fonction virtuelle final
(pour qu'elle ne puisse pas être redéfinie) ou une classe final
(pour qu'elle ne puisse pas être héritée) va permettre au compilateur d'inline des fonctions virtuelles quand il sait que la fonction appelée ne peut pas être redéfinie.
Les classes polymorphiques doivent généralement avoir un destructeur virtuel, afin de permettre aux classes filles d'implémenter un destructeur qui sera toujours appelé, même à travers la classe mère (comme pour n'importe quelle fonction virtuelle).
Passage et stockage par valeur
On ne peut pas passer les objets polymorphiques par valeur : cela appellerait le constructeur par copie de la classe de base, même en passant un type dérivé. Dans ce dernier cas, seule la classe mère de l'objet serait copiée : c'est le problème de l'object slicing.
De manière similaire, on ne peut pas stocker un objet polymorphique par valeur, car on souhaite pouvoir stocker n'importe quel type héritant d'une classe commune. Dans ce cas, on utilise généralement une indirection (comme un pointeur, ou un smart pointer) qui permet d'accéder à l'objet :
Cela amène à faire de nombreuses allocations sur le tas (new T
, make_unique<T>
, ...) et beaucoup d'indirections, ce qui ralentit le programme à la création des objets (à cause d'allocations plus lentes) et à leur utilisation (à cause d'accès en mémoire plus dispersés).
Comme on possède l'objet via une indirection vers sa classe mère, on va le détruire en appelant le destructeur de la classe mère. Il faut alors que son destructeur soit une fonction virtuelle pour éviter de ne détruire qu'une partie de l'objet (celle de la classe mère), selon le même principe que l'object slicing.
Run-Time Type Information (RTTI)
Des informations peuvent être stockées dans les tables virtuelles des classes polymorphiques pour pouvoir vérifier le vrai type d'un objet. On peut utiliser typeid
avec le type, le pointeur ou la référence souhaité pour obtenir un objet de type const std::type_info&
. Celui-ci permet d'identifier le type de façon unique :
On peut upcast les références ou pointeurs (de la classe fille vers la classe mère) en toute sécurité (via static_cast
ou implicitement). Cependant, si l'on souhaite faire un downcast (de la classe mère vers la classe fille), on peut utiliser un static_cast
, mais le type obtenu n'est pas vérifié (puisque le cast se fait à la compilation).
On peut alors à la place effectuer un dynamic_cast
qui va utiliser les RTTI des types (de départ et de destination) pour vérifier la validité du cast : si l'objet n'est pas ou n'hérite pas du type de destination, un dynamic_cast
sur un pointeur va renvoyer un pointeur null et un dynamic_cast
sur une référence va lancer une exception.
Le mot-clé friend
est utilisé à l'intérieur d'une classe, pour permettre à une fonction ou à une classe donnée de pouvoir accéder à toute la classe courante (les membres et méthodes public
, protected
et private
). Cela peut être utile pour obtenir une meilleure encapsulation :
Pour aller plus loin
Les pointeurs, les références et les smart pointers de la librairie standard peuvent changer de type, pour passer du type courant à une des classes dont hérite le type : Derived& => Base&. Il s'agit de types covariants. Si la conversion est possible dans l'autre sens, il s'agit de types contravariants.
Les tables virtuelles peuvent être redéfinies manuellement, ce qui permet de meilleures performances et/ou d'autres abstractions (comme pour la librairie link au-dessus).
Les smart pointers peuvent être utilisés par les fonctions template dynamic_pointer_cast
et static_pointer_cast
, qui correspondent respectivement à dynamic_cast
et static_cast
.
Last updated