Héritage
Le point d'héritage est de partager une interface et un protocole communs entre de nombreuses implémentations différentes de sorte qu'une instance d'une classe dérivée puisse être traitée de manière identique à toute autre instance de tout autre type dérivé.
En C ++, l'héritage apporte également des détails d'implémentation, le marquage (ou non) du destructeur comme virtuel est l'un de ces détails d'implémentation.
Liaison de fonction
Désormais, lorsqu'une fonction, ou l'un de ses cas particuliers, comme un constructeur ou un destructeur, est appelée, le compilateur doit choisir l'implémentation de la fonction voulue. Il doit ensuite générer un code machine conforme à cette intention.
La façon la plus simple de procéder consiste à sélectionner la fonction au moment de la compilation et à émettre juste assez de code machine pour que, quelles que soient les valeurs, lorsque ce morceau de code s'exécute, il exécute toujours le code de la fonction. Cela fonctionne très bien, sauf pour l'héritage.
Si nous avons une classe de base avec une fonction (pourrait être n'importe quelle fonction, y compris le constructeur ou le destructeur) et que votre code appelle une fonction dessus, qu'est-ce que cela signifie?
En prenant votre exemple, si vous avez appelé initialize_vector()
le compilateur doit décider si vous vouliez vraiment appeler l'implémentation trouvée dans Base
ou l'implémentation trouvée dans Derived
. Il y a deux façons de décider:
- La première consiste à décider que parce que vous avez appelé à partir d'un
Base
type, vous vouliez dire l'implémentation dans Base
.
- La seconde consiste à décider que, car le type d'exécution de la valeur stockée dans la
Base
valeur typée pourrait être Base
, ou Derived
que la décision quant à l'appel à effectuer, doit être prise lors de l'exécution lors de l'appel (à chaque appel).
À ce stade, le compilateur est confus, les deux options sont également valides. C'est quand virtual
vient le mix. Lorsque ce mot clé est présent, le compilateur choisit l'option 2 retardant la décision entre toutes les implémentations possibles jusqu'à ce que le code s'exécute avec une valeur réelle. Lorsque ce mot clé est absent, le compilateur choisit l'option 1 car c'est le comportement par ailleurs normal.
Le compilateur peut toujours choisir l'option 1 dans le cas d'un appel de fonction virtuelle. Mais seulement si cela peut prouver que c'est toujours le cas.
Constructeurs et destructeurs
Alors pourquoi ne spécifions-nous pas un constructeur virtuel?
Plus intuitivement, comment le compilateur choisirait-il entre des implémentations identiques du constructeur pour Derived
et Derived2
? C'est assez simple, ça ne peut pas. Il n'y a aucune valeur préexistante à partir de laquelle le compilateur peut apprendre ce qui était réellement prévu. Il n'y a pas de valeur préexistante car c'est le travail du constructeur.
Alors, pourquoi devons-nous spécifier un destructeur virtuel?
Plus intuitivement, comment le compilateur choisirait-il entre les implémentations pour Base
et Derived
? Ce ne sont que des appels de fonction, donc le comportement de l'appel de fonction se produit. Sans destructeur virtuel déclaré, le compilateur décidera de se lier directement au Base
destructeur, quel que soit le type d'exécution des valeurs.
Dans de nombreux compilateurs, si le dérivé ne déclare aucun membre de données, ni hérite d'autres types, le comportement dans le ~Base()
sera approprié, mais il n'est pas garanti. Cela fonctionnerait purement par hasard, un peu comme se tenir devant un lance-flammes qui n'avait pas encore été allumé. Tu vas bien pendant un moment.
La seule façon correcte de déclarer n'importe quel type de base ou d'interface en C ++ est de déclarer un destructeur virtuel, de sorte que le destructeur correct soit appelé pour une instance donnée de la hiérarchie de types de ce type. Cela permet à la fonction ayant la plus grande connaissance de l'instance de nettoyer cette instance correctement.
~derived()
qui délègue au destructeur de vec. Alternativement, vous supposez queunique_ptr<base> pt
connaîtrait le destructeur dérivé. Sans méthode virtuelle, cela ne peut pas être le cas. Alors qu'un unique_ptr peut recevoir une fonction de suppression qui est un paramètre de modèle sans aucune représentation d'exécution, et cette fonctionnalité n'est d'aucune utilité pour ce code.