Je crois que c'est là que nos opinions diffèrent : si le polymorphisme implique l'héritage (et encore, ça dépend de ce dont on parle), ce n'est pas réciproque pour autant.
Je comprends parfaitement ta vision des choses et le cas d'une collection d'instances de différentes classes d'une même famille est effectivement un cas extrêmement courant en POO, par exemple lorsque l'on fait la liste des widgets d'une fenêtre d'une interface graphique. Par contre, je ne considère pas ce modèle comme l'axiome fondamental de la programmation orientée objet mais seulement comme l'un de ses cas d'utilisation.
C'est également l'une des nombreuses choses qui me font apprécier le C++ plus que beaucoup d'autres langages orientés objets. Pour moi, cela va bien au delà du simple principe de « consommateur payeur ». Il s'agit réellement de travailler le plus possible sur le plan sémantique, d'une part, et de ne s'appuyer sur des entités externes au modèle qu'en dernier recours, spécialement si elles reposent sur des ressources qui ne sont disponibles qu'au runtime, d'autre part.
Non, pas si le conteneur est vraiment ce qu'il prétend être, c'est-à-dire une collection de « Fiches » et pas une interface d'accès à des objets de différents types. Et c'est bien là la seule chose requise par le LSP.... Et tu perds une partie des informations de type FicheEtendue, ce qui est inacceptable
Le cas de la destruction est particulier : d'abord parce sémantiquement, on ne demande jamais à un objet de s'auto-détruire : ça se fait soit implicitement en fin de bloc, soit via l'opérateur delete et dans ce dernier cas, l'opérateur est statique, même s'il peut être défini au sein de la classe.
Ensuite, parce que ça implique au moins un niveau d'indirection et de passer par des pointeurs explicites, non seulement pour pouvoir référencer de manière uniforme des objets de taille différentes et inconnues à l'avance, mais également pour pouvoir les détruire, avec delete donc. Si tu adoptes le principe du « zéro pointeur tout référence » souvent préconisé en C++ même s'il est très difficile à respecter totalement, tu ne peux pas rendre ce service, sauf à retrouver l'adresse de l'objet référencé avec « &xxx », à supposer qu'il a effectivement été instancié dans le tas avec new et à rendre la référence invalide passé ton delete et jusqu'à la fin du bloc ou elle est définie.
Oui, mais comme dit plus haut, si tu ne te bases pas sur ce contenu pour les détruire mais que tu laisses ce soin aux mécanismes qui ont instancié les objets dérivés, ça reste quand même de l'héritage.Mais si tu te base sur le contenu de ton conteneur pour les détruire (par exemple, au moment du grand "clean up" avant de quitter l'application), ce sera ce qui est pointé par des... pointeurs sur le type de base que tu essayeras de détruire, et, si le destructeur est public et non virtuel, seuls les membres correspondant à la classe de base seront correctement détruits
C'est ce que j'essayais de dire avec ma lib externe : si tu utilises un objet A défini dans une bibliothèque que tu n'as pas écrite et dont ni les fonctions-membres ordinaires ni les constructeurs n'ont été virtualisés, tu peux quand même étendre A vers B et te baser sur ce nouveau type dans tes propres programmes sans avoir à tout réécrire. Tu peux même passer tes objets aux fonctions de la bibliothèque originale, tant que celle-ci ne réclame pas leur destruction.
Ce qui me conduit à la conclusion qu'en fin de compte, il n'y a vraiment que le cas de la destruction d'objet qui puisse lier LSP, héritage et virtualité.J'en suis conscient, mais la destruction d'un objet se doit, malheureusement, s'adapter au type réel de l'objet. Ce n'est pas parce que tu ne crées pas explicitement un destructeur qu'il n'y en a pas, bien au contraire: Le compilateur en rajoute d'office un qui est public et non virtuel Tout comme le compilateur rajoute un constructeur par défaut, un constructeur par copie et un opérateur d'affectation si l'on ne prend pas les dispositions pour l'en empêcher C'est, justement, pour prendre cette particularité en compte que la notion de POD a été revue dans la norme
Ça vaut le coup de s'y attarder : d'un côté, je ne peux pas passer un « B * » à un delete conçu pour « A * » si mon destructeur n'est pas virtuel sans fuite de mémoire dans des conditions courantes d'utilisation. Donc, non substituabilité : LSP par terre.
De l'autre côté : peut-on réellement considérer la destruction d'un objet comme une de ses propriétés ? C'est ce que je sous-entendais dans mes précédents paragraphes. Sur le plan sémantique et syntaxique, c'est une action extérieure et il n'y a plus de sens à vérifier si une propriété de l'objet est vraie ou fausse si l'objet n'existe plus.
En outre, lorsque le destructeur d'un objet A n'est pas virtuel, il s'engage à faire le ménage nécessaire dans sa propre classe mais en aucun à garantir celui des classes dérivées. Ça n'a jamais fait partie de son contrat. De fait, un delete sur un pointeur « A * » qui pointe en fait un B, même s'il laisse dans le vide les ressources utilisées par B (ce qui, bien sûr, n'est pas souhaitable), remplit toutes les conditions connues et attendues à la compilation à l'endroit où le delete est écrit. De ce côté là, le LSP est toujours debout.
En outre, il s'agit d'une nécessité due à la façon dont le C++ gère ses objets, mais ce n'est pas une caractéristique universelle de tous les systèmes informatiques proposant un mécanisme d'héritage.
Ces trois derniers points me laissent penser que le cas de la destruction d'un objet est probablement en dehors du périmètre du LSP. Et si ce n'est pas le cas, alors il s'agit d'une exception.
Certes, mais rien t'empêche soit d'ajouter un numéro de séquence unique en utilisant une variable static à la classe mère, soit d'utiliser les pointeurs vers les instances respectives pour voir s'il s'agit ou non de la même fiche, soit encore de faire les deux. Dans tous ces cas, aucun recours à une quelconque fonction virtuelle n'est requise. Sauf, encore une fois, dans le cas de la destruction.Mais, justement, il te faut un facteur discriminant permettant d'identifier de manière unique et non ambigüe une fiche particulière parmis les différents homonymes qu'il peut y avoir...
Si, voulant modifier la fiche de Gerard Lambert de Calais, boucher de son état, tu en viens à modifier la ficher de Gerard Lambert de Paris, Carrossier de son état, il y aura un problème...
Nous sommes donc bel et bien face à la définition d'une classe ayant sémantique d'entité : deux instances de la classes peuvent parfaitement avoir des attributs strictement identiques mais représenter des entités diffférentes, et donc seront considérées différentes si l'adresse mémoire à laquelle elles se trouvent sont différentes
Avec le destructeur public et non virtuel, j'ai déjà démontré que le comportement de destruction n'est pas correct dans certaines situations.
Tu pourrais, d'un autre coté, éviter la virtualité si le destructeur de Fiche était protégé, mais
Si le destructeur de Fiche est protétgé, le compilateur t'envoie une erreur sur un code aussi simple que
ce qui fait que tu ne peux pas créer d'instance de Fiche, vu que tu ne peux pas la détruire.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 void foo() { Fiche mafiche; /* ... */ } //Erreur ici : tentative d'utiliser le destructeur de mafiche qui est protégé dans ce contexte
Si ce n'est pas mettre une restriction forte à l'utilisation de Fiche, dis moi ce que c'est
À moins que tu te réfères au fait de stocker des copies de la classe de base dans le container. Là, bien sûr, ça crée des individus supplémentaires qui ne sont pas souhaitables si tu te places dans la sémantique d'entité. Mais là, tu peux t'en tenir aux pointeurs ou aux références et à vrai dire, ça ne t'oblige même pas à les dériver.
Si, en revanche, on considère que l'information est donnée par la nature même de l'instance, donc au type dérivé auquel elle appartient, c'est une information qu'on ne peut pas obtenir non plus, sauf à faire du RTTI ou à implémenter une méthode maison pour s'identifier. Mais dans ce cas, ça ne concerne pas le LSP ni l'héritage en général. Par ailleurs, quand on en est là, il vaut mieux changer de repère et passer à un modèle « tout interprété », ce qui aura le mérite d'apporter tout ce qui va avec, comme l'introspection.
D'ailleurs, le paradigme qui repose sur une couche interprétée, où tout fonctionne par transmission de référence et où toutes les fonctions sont virtuelles par défaut est celui adopté par Java, de façon totalement avouée.
Partager