Citation:
Envoyé par
Obsidian
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.
Où ai-je dit que c'était réciproque :question: j'ai juste dit que le polymorphisme est indissociable de l'héritage, et j'ai justifié cette phrase parle fait que la "destructibilité" de l'objet est un comportement qui doit pouvoir s'adapter au type réellement utilisé quand celui-ci passe pour etre du type de la classe de base.
Citation:
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.
Mais, si c'est un conteneur de fiche (sous entendu, ou est ce que je me trompe :question: sous forme de valeur), tu perds une partie d'information si tu essaye d'y introduire une (copie de) FicheEtendue, et c'est inacceptable.
Citation:
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.
On ne lui demande pas de s'auto-détruire, mais l'un des services qu'il doit rendre est d'être correctement destructible, nuance ;)
Citation:
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.
J'applique plus volontiers le principe de "la référence chaque fois que possible, le pointeur quand on n'a pas le choix", c'est plus souple comme principe ;)
Citation:
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 il y aura, de toutes façons un mécanisme de destruction à prévoir "quelque part", surout que, malgré tout, ta classe a sensiblement sémantique d'entité, vu qu'il faut un facteur discriminant ;)
Citation:
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.
Non, a priori, si une lib externe fournit une classe sans destructeur virtuel public (ou sans destructeur protégé et non virtuel), c'est que la classe en question n'a pas vocation à être dérivée, et tout ce que tu peux envisager, c'est l'agrégation, quitte à "déporter" les fonctions qui t'intéressent dans cette classe dans l'interface de la tienne ;)
Citation:
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é.
Il n'y a peut etre que cela, mais c'est déjà énorme, vu qu'il s'agit d'un comportement tout aussi indispensable que celui qui consiste à respirer pour l'homme ;)
Citation:
Ç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.
N'inversons pas les rôles ni les étapes...
Ce n'est pas LSP qui doit se plier à ce que tu veux, mais c'est bien toi qui doit veiller à le respecter ;)
En outre, comme je l'ai maintenu tout au long d'une discussion entrée dans les annales, LSP est un principe de conception qui doit, en tant que tel, intervenir très tot dans le processus et dont le respect est une condition sine qua non à toute possibilité d'héritage...
Citation:
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.
La destruction est une action extérieure, mais la "destructibilité" (comprends : le comportement permettant à l'objet de faire correctement le ménage de tous les membres qu'il utilise) fait, bien évidemment, partie de ses propriétés : cf les formes canoniques de coplien ;)
En outre, lorsque le destructeur d'un objet A n'est pas virtuel, il
Citation:
s'engage à faire le ménage nécessaire dans sa propre classe mais en aucun à garantir celui des classes dérivées.
Une classe de base n'a, de toutes façons, jamais à prendre le moindre engagement vis à vis des classes dérivées...
Mais cela ne veut pas dire qu'elle ne doit pas accepter que l'une des classes dérivées puisse adapter l'un de ses comportement en fonction du type réellement utilisé
Citation:
Ça n'a jamais fait partie de son contrat.
On est bien d'accord là dessus ;)
Citation:
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.
Non, si tu as un destructeur public et non virtuel dans la classe de base et que tu appelle delete sur un pointeur de la classe de base, tu as un comportement indéterminé...
C'est peut etre la pire chose qui puisse t'arriver lorsque tu fais de la programmation ;)
Citation:
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.
C'est surtout parce que C++ est le seul langage récent proposant l'héritage à ne pas rendre les fonctions virtuelles par défaut (et à donner éventuellement un mécanisme "terminal" ou similaire indiquant qu'une fonction n'a plus vocation à être redéfinie)...
Mais la virtualité des fonctions, ou tout mécanisme pouvant s'en rapprocher, et le polymorphisme en général nécessite qu'une fonction déclarée dans la classe de base puisse éventuellement pouvoir adapter son comportement au type réellement manipuler quand on a affaire à un objet "passant pour etre" du type de la classe de base.
Du moins, c'est le cas de tous les langages récents (car, ne connaissant réellement ni Ada ni SmallTalk, je n'ose pas m'exprimer à leur sujet ;))
Citation:
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.
Il n'est pas du tout en dehors du périmètre de LSP, à partir du moment où il fait partie de l'interface publique (et qu'il peut donc être considérée comme étant une propriété valide de la classe de base) et, non, il ne s'agit pas d'une exception car toute fonction faisant partie de l'interface publique de la classe de base est à considérer comme étant propriété valide de la classe de base, et se doit donc d'être également valide pour les sous types.
Maintenant, il y a, effectivement, des comportements qui ne nécessitent absolument aucune adaptation au niveau des sous types et que l'on peut donc laisser non virtuels (si tant est que l'on donne un comportement cohérent à la classe de base).
Mais, pour que les comportements qui n'entrent pas dans la catégorie que je viens de citer puissent être considérés comme étant "propriété valide" du sous type, il faudra (oserais-je dire "d'office") permettre qu'ils s'adaptent en fonction du type dynamique de l'objet manipulé.
Citation:
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.
Le problème, c'est que même s'il n'y a "que" la destruction qui doit s'adapter (parce que tu déciderais volontairement de fouler au pied le principe d'encapsulation / de ségrégation de l'interface), étant donné que c'est un comportement majeur et incontournable que doit offrir tout type que tu peux créer, tu n'as pas vraiment le choix, dés le moment où tu envisages qu'une classe puisse servir de base à des sous types ;)
Citation:
À 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.
Comme je l'ai dit, stocker dans un conteneur des Fiche par valeur est inacceptable si tu décide de travailler avec des FicheEtendue, du fait de la perte d'information que cela entraine ;)
Citation:
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.
C'est pour cela que tu as des mécanismes comme l'encapsulation et le fait de penser en terme de services rendus, pouvant éventuellement s'adapter au type réel de l'objet manipulé...