OVH Cloud OVH Cloud

Polymorphisme ?

28 réponses
Avatar
PurL
Bonjour,

Je voudrais faire un truc, et d'un je sais pas comment faire et dedeux je
crois que ce que je veux faire s'appelle le polymorphisme.
Explications :

J'ai une classe parent et des classes enfants dérivés de parent.
Toutes les instances de mes classes enfants sont stockées dans une liste de
pointeurs.
Quand je veux accéder à une variable membre commune à toutes les classes
enfants par héritage de la classe parent, je caste avec le type parent :

(parent *)list(i)->ma_variable = 0;

Par contre, j'aimerais appeler une fonctions membre dont le prototype est
présent dans la classe parent et redéclarée dans toutes les classes enfants
car pour chaque classe enfant le code de cette fonction diffère. Mais je ne
peux pas faire :

(parent *)list(i)->ma_fonction();

car j'appelle celle de parent, et je ne peux pas caster avec le type enfant
correspondant car je ne sais quel pointeur de la liste va etre utilisé...

Comment faire pour que :

list(i)->ma_fonction();

appelle la fonction membre de la classe enfant désigné par le pointeur
list(i) ?

Merci,

PurL

10 réponses

1 2 3
Avatar
kanze
"Christophe Lephay" wrote in message
news:<bp2m9p$amd$...
Benoit Rousseau wrote:

J'ai lu quelque part dans ces news qu'il fallait déclarer le
destructeur de base comme virtuel ? C'est Bjarne qui a dit ca je
crois dans son bouquin aussi. Dans quelles conditions est-ce vrai ?
(J'ai pas le bouquin et pas l'argent pour l'acheter non plus)


Dès que tu utilises le polymorphisme, il y a de grandes chances que tu
l'utilises via des pointeurs.


Ou des références. Le polymorphisme ne fonctionne pas autrement.

Dans ce cas, il est probable que tu effaces l'objet dérivé par un
pointeur sur la classe de base, et c'est le destructeur de la classe
de base qui sera donc appelé à moins que ce dernier n'y soit déclaré
virtuel.


Au moins que toutes les instances de la classe (et des classes dérivées)
soient allouées sur la pile, ou statiquement. (Les deux cas me sont déjà
arrivés.) Mais c'est une risque qui ne vaut pas la peine.

Et le delete, c'est un comportement indéfini. C'est possible que le seul
effet serait que le destructeur de la classe dérivée ne serait pas
appelé, mais bien plus souvent, à juger au moins d'après les
implémentations que je connais, on appelera parfois la fonction operator
delete avec le mauvais pointeur, c-à-d avec autre chose que ce qu'a
renvoyé la fonction operator new, ce qui mène souvent à la corruption de
la gestion de la mémoire dynamique.

Tu n'as pas le problème si tu "polymorphises" à travers des
références, vu que tu n'as pas véritablement création et destruction
d'objets par ce biais, mais références et pointeurs sont si proches
sémantiquement parlant, que ce serait un peu hasardeux de demander aux
utilisateurs de ta hiérarchie de ne pas utiliser de pointeurs mais que
des références.


Que tu utilises des références ou des pointeurs ne changent absolument
rien. Sauf qu'il faut que le paramètre de delete soit un pointeur. Or,
dans l'expression « delete p », si le type dynamique de *p est différent
du type statique, ou bien le type statique a un destructeur virtuel, ou
bien il y a un comportement indéfini.

Conclusion : dès que tu veux du polymorphisme (notemment dès qu'une
fonction est virtuelle), il faut fournir un destructeur virtuel (même
si on peut toujours imaginer des cas improbables où c'est inutile)...


La règle que donne Herb Sutter, c'est ou bien, la classe a un
destructeur virtuel, ou bien elle a un destructeur protégé, ou bien, tu
n'en dérives pas.

Quand tu écris une classe qui ne dérive de rien, une bonne règle de
base, c'est que s'il y a une fonction virtuelle, le destructeur doit
être virtuel.

--
James Kanze GABI Software mailto:
Conseils en informatique orientée objet/ http://www.gabi-soft.fr
Beratung in objektorientierter Datenverarbeitung
11 rue de Rambouillet, 78460 Chevreuse, France, +33 (0)1 30 23 45 16


Avatar
kanze
"Bertrand Motuelle" wrote in message
news:<bp3h9e$kq6$...
"Christophe Lephay" schrieb im
Newsbeitrag news:bp2m9p$amd$
Benoit Rousseau wrote:

Dès que tu utilises le polymorphisme, il y a de grandes chances que
tu l'utilises via des pointeurs. Dans ce cas, il est probable que tu
effaces l'objet dérivé par un pointeur sur la classe de base, et
c'est le destructeur de la classe de base qui sera donc appelé à
moins que ce dernier n'y soit déclaré virtuel.


Dans la pratique, c'est généralement ce qui se passe (seul le
destructeur de la classe de base est appelé). Mais formellement c'est
un comportement indéfini.


Tu l'as essayé avec combien de compilateurs ? Disons avec le code
suivant :

#include "gb/iostream.hh"
#include "gb/ostream.hh"
#include "gb/MemoryCheck.hh"

struct VB
{
~VB() ;
} ;

VB::~VB()
{
std::cout << "~VB" << std::endl ;
}

struct L : virtual VB
{
~L() ;
} ;

L::~L()
{
std::cout << "~L" << std::endl ;
}

struct R : virtual VB
{
~R() ;
} ;

R::~R()
{
std::cout << "~R" << std::endl ;
}

struct D : L, R
{
~D() ;
} ;

D::~D()
{
std::cout << "~D" << std::endl ;
}


int
main()
{
std::cout << "Starting test" << std::endl ;

GB_MemoryCheck check ;
VB* pvb = new D ;
std::cout << "par VB*" << std::endl ;
delete pvb ;

L* pl = new D ;
std::cout << "par L*" << std::endl ;
delete pl ;

R* pr = new D ;
std::cout << "par R*" << std::endl ;
delete pr ;

if ( check.unfreedCount() != 0 ) {
std::cout << "Memory leak: " << std::endl ;
check.display( std::cout ) ;
}

return 0 ;
}

J'ai une erreur fatale (core dump, échec d'une assertion, etc.) avec
tous les compilateurs que j'ai essayés.

--
James Kanze GABI Software mailto:
Conseils en informatique orientée objet/ http://www.gabi-soft.fr
Beratung in objektorientierter Datenverarbeitung
11 rue de Rambouillet, 78460 Chevreuse, France, +33 (0)1 30 23 45 16


Avatar
Christophe Lephay
wrote:
"Christophe Lephay" wrote in message
Dès que tu utilises le polymorphisme, il y a de grandes chances que
tu l'utilises via des pointeurs.


Ou des références. Le polymorphisme ne fonctionne pas autrement.


Oui, je parle des références plus loin dans mon post...

Dans ce cas, il est probable que tu effaces l'objet dérivé par un
pointeur sur la classe de base, et c'est le destructeur de la classe
de base qui sera donc appelé à moins que ce dernier n'y soit déclaré
virtuel.


Au moins que toutes les instances de la classe (et des classes
dérivées) soient allouées sur la pile, ou statiquement. (Les deux cas
me sont déjà arrivés.) Mais c'est une risque qui ne vaut pas la peine.


Plus que le destructeur, c'est le delete qui pose problème si il se fait via
un pointeur sur base. C'est pour celà que je distinguais pointeurs et
références...

Tu n'as pas le problème si tu "polymorphises" à travers des
références, vu que tu n'as pas véritablement création et destruction
d'objets par ce biais, mais références et pointeurs sont si proches
sémantiquement parlant, que ce serait un peu hasardeux de demander
aux utilisateurs de ta hiérarchie de ne pas utiliser de pointeurs
mais que des références.


Que tu utilises des références ou des pointeurs ne changent absolument
rien. Sauf qu'il faut que le paramètre de delete soit un pointeur. Or,
dans l'expression « delete p », si le type dynamique de *p est
différent du type statique, ou bien le type statique a un destructeur
virtuel, ou bien il y a un comportement indéfini.


Ce que je voulais souligner, c'est que les références ne font appel ni au
constructeur, ni au destructeur, ce qui fait que leur utilisation est moins
problematique vis à vis d'un destructeur qui ne serait pas virtuel...

Conclusion : dès que tu veux du polymorphisme (notemment dès qu'une
fonction est virtuelle), il faut fournir un destructeur virtuel (même
si on peut toujours imaginer des cas improbables où c'est inutile)...


La règle que donne Herb Sutter, c'est ou bien, la classe a un
destructeur virtuel, ou bien elle a un destructeur protégé, ou bien,
tu n'en dérives pas.

Quand tu écris une classe qui ne dérive de rien, une bonne règle de
base, c'est que s'il y a une fonction virtuelle, le destructeur doit
être virtuel.


J'avais précisé *notemment* dans "notemment dès qu'une fonction est
virtuelle", pour inclure le cas où on souhaiterait un comportement
polymorphe alors même qu'aucune fonction virtuelle ne serait définie (bien
que l'utilité en soit nettement restreinte)...

Chris


Avatar
PurL
Quand tu écris une classe qui ne dérive de rien, une bonne règle de
base, c'est que s'il y a une fonction virtuelle, le destructeur doit
être virtuel.


Et si la classe dérivée n'a rien à detruire de plus que la classe de base,
le destructeur n'a pas besoin d'etre virtuel ?

PurL

Avatar
Jean-Marc Bourguet
"PurL" writes:

Quand tu écris une classe qui ne dérive de rien, une bonne règle
de base, c'est que s'il y a une fonction virtuelle, le destructeur
doit être virtuel.


Et si la classe dérivée n'a rien à detruire de plus que la classe de
base, le destructeur n'a pas besoin d'etre virtuel ?


Formellement, des que tu detruit a partir d'un pointeur sur la classe
de base, tu en as besoin.

En pratique il est possibles que ca marche dans certains cas bien
specifiques, du genre heritage simple et non virtuel sur toute la
ligne et aucun membre nouveau dans la classe derivee. Mais si tu
trouves un compilateur ou ca ne marche pas, il a peut-etre de bonnes
raisons.

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
Christophe Lephay
PurL wrote:
Quand tu écris une classe qui ne dérive de rien, une bonne règle de
base, c'est que s'il y a une fonction virtuelle, le destructeur doit
être virtuel.


Et si la classe dérivée n'a rien à detruire de plus que la classe de
base, le destructeur n'a pas besoin d'etre virtuel ?


Le problème étant que la classe de base ne peut pas garantir ni pré-supposer
qu'une classe dérivée n'aura jamais rien de plus à détruire. De toute façon,
tu restes dans le comportement indéfini...

Chris


Avatar
Bertrand Motuelle
wrote:
"Bertrand Motuelle" wrote in message
news:<bp3h9e$kq6$...

Dans la pratique, c'est généralement ce qui se passe (seul le
destructeur de la classe de base est appelé). Mais formellement c'est
un comportement indéfini.



Tu l'as essayé avec combien de compilateurs ? Disons avec le code
suivant :


[snip bout de code avec heritage en losange]

J'ai une erreur fatale (core dump, échec d'une assertion, etc.) avec
tous les compilateurs que j'ai essayés.


Je n'ai jamais eu besoin de construire ce genre de hierarchie.
Donc je n'ai jamais essaye un cas tel que tu l'as poste (et de toute
facon je declare le destructeur de la classe virtuel quand je veux
documenter qu'une classe peut servir de classe de base :-)

Je parlais d'heritage simple.
#include <iostream>
#include <ostream>

struct B { ~B() { std::cout << "~B" << std::endl; } };
struct D : B { ~D() { std::cout << "~D" << std::endl; } };

int main()
{
B* b = new D();
delete b;
}

Et la sun CC4.2, CC5.3, CC5.4, g++2.95.? et g++3.2.2 se contentent
d'appeler le destructeur de la classe de base.

Merci pour l'exemple, je vais le conserver precieusement!

Bertrand.


Avatar
kanze
Bertrand Motuelle wrote in message
news:<bpb9t9$28k1$...
wrote:
"Bertrand Motuelle" wrote in message
news:<bp3h9e$kq6$...

Dans la pratique, c'est généralement ce qui se passe (seul le
destructeur de la classe de base est appelé). Mais formellement
c'est un comportement indéfini.


Tu l'as essayé avec combien de compilateurs ? Disons avec le code
suivant :


[snip bout de code avec heritage en losange]

J'ai une erreur fatale (core dump, échec d'une assertion, etc.)
avec tous les compilateurs que j'ai essayés.


Je n'ai jamais eu besoin de construire ce genre de hierarchie. Donc je
n'ai jamais essaye un cas tel que tu l'as poste (et de toute facon je
declare le destructeur de la classe virtuel quand je veux documenter
qu'une classe peut servir de classe de base :-)


Tout à fait. Moi, ça ne m'arrive pas tous les jours non plus (mais c'est
bien arrivé de temps en temps). Mais évidemment, je l'ai fait exprès
pour être sûr que tous les compilateurs s'y plantent. En général, il en
faut bien moins -- le moindre héritage multiple suffit souvent.

Je parlais d'heritage simple.


Certes, mais tu ne l'as pas dit.

#include <iostream>
#include <ostream>

struct B { ~B() { std::cout << "~B" << std::endl; } };
struct D : B { ~D() { std::cout << "~D" << std::endl; } };

int main()
{
B* b = new D();
delete b;
}

Et la sun CC4.2, CC5.3, CC5.4, g++2.95.? et g++3.2.2 se contentent
d'appeler le destructeur de la classe de base.


Dans la pratique. Mais montre le cas où un comportement indéfini
fonctionne n'est pas le bon moyen à décourager les gens de s'en
servir:-).

En fait, si c'est assez usuel que l'adresse de la classe de base soit la
même que l'adresse de l'objet entier, il n'y a rien dans la norme qui
l'exige, et une implémentation pourrait très bien ne pas fonctionner
dans ce cas-ci. Je n'en connais pas qui le fait, mais je n'exclurais pas
qu'il en existe.

Plus généralement, dans la pratique, si les héritages en lozange ne sont
pas si courants, les héritages multiples sont prèsque la règle dans
certains domains d'application (GUI, etc.). Et avec la plupart des
compilateurs, on aurait des problèmes avec les héritages multiples. Pas
à tous les coups, évidemment ; seulement de temps en temps.

Merci pour l'exemple, je vais le conserver precieusement!


En passant, un programme assez simple, même avec l'héritage en lozange,
pourrait sembler fonctionner -- souvent, le fait de passer une mauvaise
adresse à la fonction operator new (et donc, en général, à malloc) soit
ne fait rien (ce qui veut dire qu'il y a ici une fuite de mémoire, mais
ton programme ne le détectera pas), soit il corromp l'espace du tas (ce
qui ferait peut-être un core à la prochaine allocation, mais tu ne fais
pas d'allocation par la suite).

C'est pourquoi je me suis servi de GB_MemoryCheck. Faute de Purify, ou
quelque chose du genre.

C'est juste pour dire qu'il faut se méfier des comportements indéfinis,
même quand ils ont l'air de marcher.

--
James Kanze GABI Software mailto:
Conseils en informatique orientée objet/ http://www.gabi-soft.fr
Beratung in objektorientierter Datenverarbeitung
11 rue de Rambouillet, 78460 Chevreuse, France, +33 (0)1 30 23 45 16



Avatar
kanze
"Christophe Lephay" wrote in message
news:<bpau00$2qt$...
PurL wrote:

Quand tu écris une classe qui ne dérive de rien, une bonne règle de
base, c'est que s'il y a une fonction virtuelle, le destructeur
doit être virtuel.


Et si la classe dérivée n'a rien à detruire de plus que la classe de
base, le destructeur n'a pas besoin d'etre virtuel ?


Le problème étant que la classe de base ne peut pas garantir ni
pré-supposer qu'une classe dérivée n'aura jamais rien de plus à
détruire. De toute façon, tu restes dans le comportement indéfini...


Le problème, et c'est ce qu'on s'entête à dire, n'a rien à voir avec
l'appel ou non du destructeur de la classe dérivée. C'est un
comportement indéfini, et c'est un comportement indéfini qui ne marche
réelement pas dans bien de cas. Le problème, c'est que sur des petits
exemples, on ne voit pas forcement qu'on a bousillé le tas, et qu'une
allocation on ne sait pas trop quand dans l'avenir va foirer, ou qu'il y
a une fuite de mémoire, ou que sais-je.

Toute explication qui se base sur ce qui se fait, ou sur ce qui pourrait
se faire, dans le destructeur de la classe dérivée, est radicalement
incomplète.

--
James Kanze GABI Software mailto:
Conseils en informatique orientée objet/ http://www.gabi-soft.fr
Beratung in objektorientierter Datenverarbeitung
11 rue de Rambouillet, 78460 Chevreuse, France, +33 (0)1 30 23 45 16



Avatar
Christophe Lephay
wrote:
"Christophe Lephay" wrote in message
news:<bpau00$2qt$...
Et si la classe dérivée n'a rien à detruire de plus que la classe de
base, le destructeur n'a pas besoin d'etre virtuel ?


Le problème étant que la classe de base ne peut pas garantir ni
pré-supposer qu'une classe dérivée n'aura jamais rien de plus à
détruire. De toute façon, tu restes dans le comportement indéfini...


Le problème, et c'est ce qu'on s'entête à dire, n'a rien à voir avec
l'appel ou non du destructeur de la classe dérivée. C'est un
comportement indéfini, et c'est un comportement indéfini qui ne marche
réelement pas dans bien de cas. Le problème, c'est que sur des petits
exemples, on ne voit pas forcement qu'on a bousillé le tas, et qu'une
allocation on ne sait pas trop quand dans l'avenir va foirer, ou
qu'il y a une fuite de mémoire, ou que sais-je.

Toute explication qui se base sur ce qui se fait, ou sur ce qui
pourrait se faire, dans le destructeur de la classe dérivée, est
radicalement incomplète.


C'est pour celà que j'avais rajouté "De toute façon, tu restes dans le
comportement indéfini" :-/

Chris



1 2 3