OVH Cloud OVH Cloud

constructeur et patron de méthode, impossible ?

20 réponses
Avatar
meow
Hello,

Un probl=E8me tout simple :
J'ai une hi=E9rarchie de classes dont le constructeur instancie les
champs priv=E9s (quelle originalit=E9, n'est-ce pas ?), et finit
l'initialisation en appelant une m=E9thode qui diff=E8re suivant la
classe. =E7a ressemble donc =E0 un patron de m=E9thode :


class A{
private :
B b;
public :
A(B &bb):b(bb){ maMethode(); }
private :
virtual void maMethode()=3D0;
}

class AA:public A{
public :
AA(B &bb):A(bb){}
// avec une d=E9finition correcte de maMethode()
}

Ce qui ne marche pas parce que le linker semble chercher
A::maMethode()...
Suis-je condamn=E9 =E0 descendre l'appel =E0 maMethode dans le corps du
constructeur de AA et des autres classes filles ? Une autre id=E9e ?

--Ben

10 réponses

1 2
Avatar
kanze
Arnaud Meurgues wrote:
kanze wrote:

Comme tu sais, le langage n'impose pas de vtable:-). Mais en
effet, c'est à peu près comme ça dans toutes les
implémentations que je connais.


Oui. Je n'ai jamais réussi à retenir les solutions
alternatives. Je crois qu'elles ont déjà été évoquées ici
(est-ce smalltak qui utilise un autre mécanisme ?) mais ma
mémoire refuse de s'en souvenir.


Un tableau haché des fonctions vient à l'esprit. C'est (ou
c'était) l'implémentation préférée de Smalltalk. Mais Smalltalk
n'est pas du C++ ; il n'y a pas d'obligation à declarer les
fonctions virtuelles dans la classe de base, par exemple. Alors
du coup, on ne sait pas construire un vtable sans voir le
programme complet.

Les alternatives sont-elles vraiment moins bonnes, ou bien la
vtable s'est-elle seulement imposée pour des raisons pratiques
(du genre réutilisation du linker C) ?


Je crois que pour le C++, la vtable, c'est vraiment ce qu'on a
trouvé de mieux. Sauf, évidemment, qu'elle n'offre pas de
solution évidente pour les fonctions templatées virtuelles.

constructeur de T. (En Java, c'est très facile de se trouver
dans une fonction sur un objet dont le constructeur n'a pas
encore été appelé.)


Ça, je ne le savais pas. Ça me paraît plus que curieux et je
suis content de l'apprendre (j'ai peu eu à développer en Java
et n'ai jamais été confronté à cette situation).


J'avoue que je l'ai appris « the hard way ». Je savais
abstraitement que les règles étaient différentes qu'en C++, mais
je n'y avais pas prété beaucoup d'attention. Jusqu'à ce que
j'aie dérivé d'une classe dans Swing, et que mon objet ne s'est
pas correctement affiché, parce que le constructeur de la classe
de Swing a appelé une de mes fonctions, qui dépendait d'une
initialisation faite dans le constructeur. J'ai eu donc un
NullPointerException, qui a disparu silentieusement dans la
boucle des évenemments de Swing. Il a fallu un certain temps
pour savoir ce qui n'allait pas.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
Sylvain
kanze wrote on 12/07/2006 09:19:

donc que la "fonction soit appelée dans le cst ou ailleurs"
est inexact.


Non seulement tu ne connais pas le C++, tu ne veux pas
comprendre.


non seulement tu ne connais pas l'utilisation du C++ (seulement enclin à
nous recopier (parfois l'expliquer) la norme), mais tu ne veux pas
comprendre.

comprendre qu'ici la réalisation de l'appel (pas sa logique telle que
définie par la norme) est différente *parce que l'on est dans un
constructeur*; nous rerépétez que ce serait tjrs pareil se heurte à
l'expérience et n'apprend rien au final.

Jusqu'ici, je n'ai pas vu d'alternatif qui tient la route. La
solution de Java est une source grave d'erreurs, et ne marche
pas dans la pratique. Qu'est-ce que tu proposes d'autre ?


rien, il s'agissait pas de proposer des alternatives (mais si le mode de
Java, source d'écritures plus courtes et sures existe); je sais ce que
fait C++ donc je fais avec, je me gratte pas 107 ans des comparaisons
emprunté de ma norme (C++) est mieux que ta norme (Java), ces
comparaisons n'amènent à rien quand on les utilise d'aussi mauvaise foi.

Le problème, d'après mon expérience (concrète), c'est plutôt
avec la façon que fait Java, où on se trouve dans une
fonction membre sur un objet dont le constructeur n'a pas
encore été appelé. C'est une source d'erreurs importante.




si tu écris des trus comme:

class Machin {
Truc truc;
Machin(){
maMethodeQuiTrucMuche();
truc = new Truc();
}
void maMethodeQuiTrucMuche(){
truc.muche();
}
};

t'as en effet un problème, mais pas de langage, de codage

Je vois que tes connaissances de Java sont aussi faible que
celles du C++. Essaie de dériver d'une classe de Swing, par
exemple, et de supplanter certaines fonctions virtuelles. Tu te
rétrouves vite dans la fonction sans que le constructeur a été
appelé, et avec toutes les variables membre à null.


- je supplante tous les jours de nombreuses méthodes des classes de
javax.swing (ou d'autres packages) sans péter des NullPointerException
par erreur de codage,

- que /je/ sois "faible" parce que /tu/ génères de telles erreurs est
risible et pitoyable

- tes attaques ad homimem et répétitives sont hors chartre et sans aucun
intérêt, un jour peut être te lasseras-tu, pour ma part et depuis le
primaire j'ai toujours laissé autrui gagner quand il s'agit de jouer au
plus con.

hein ? quel rapport ? gérer un delete this au milieu du
destructeur ?


Tiens... Tu n'as jamais entendu parler des exceptions ? Elles
existent pourant, et en Java et en C++. En Java, en cas


sans blague ? vous les avez ajouté, dire que je les gérais à la main
quand CFront ne les connaissais pas.

d'exception, on laisse tout comme il était, et on espère pour le
mieux. C'est quasiment impossible d'écrire un programme
« correct » dans de tels cas, mais qu'importe.


autre lacune Java ? je te rapelle qu'il y a un GC pour récupérer "tout"
et que tu as le droit de supplanter finalize() si besoin.

En C++, en cas d'exception,
on appelle des destructeurs des sous-objets déjà construits.


a) donc j'avais faux d'envisager un delete dans le constructeur ...
c'est pas delete, c'est "delete-dit-par-James" ...

b) "on" appele même un delete sur les objects-membre alloués, voui, voui
et on s'emmerde encore à écrire proprement des destructeurs ...

faudrait arréter les amalgames, tu crois pas ? un handler d'expection
doit être codé correctement (re)"point".

Qu'est-ce que les instances statiques aient à voir ici.


cela t'éviterait une NullPtrExc. quand tu oublies d'appeler un cst.

-- et obtenir une NullPointerException est justement agréable,
Par rapport à quoi ?



relis ton post (un cran au-dessus)

La norme laisse la liberté aux implémentations de faire quelque
chose d'intelligent. Mais effectivement, peu le font (VC++, je
crois, mais je crois que c'est le seul).


non, non, il ne fait rien d' "intelligent".

Sylvain.



Avatar
Loïc Joly

comprendre qu'ici la réalisation de l'appel (pas sa logique telle que
définie par la norme) est différente *parce que l'on est dans un
constructeur*; nous rerépétez que ce serait tjrs pareil se heurte à
l'expérience et n'apprend rien au final.


Je n'ai jamais entendu James dire que c'était pareil. Juste que c'était
quand même un appel virtuel, mais qu'il fallait faire attention que le
type considéré était le type courant, et non le type final.

Ce qui a le mérite d'être vrai, alors que dire que dans un constructeur,
on considère le type statique, que le mécanisme de virtualité est
supprimé, est faux.

Les exemples déjà cités le prouvent. Je trouve pour ma part l'exemple
plus parlant écrit ainsi :

#include <iostream>
using namespace std;

struct A
{
void f() {g();}
virtual void g() {cout << "A" << endl;}
};

struct B : A
{
B() {f();}
virtual void g() {cout << "B" << endl;}
};

struct C : B
{
virtual void g() {cout << "C" << endl;}
};

int main()
{
C c;
}

Si on prenait en compte le type statique, on verrait "A", si c'était le
type dynamique, on verrait "C". Or ce qu'on voit est "B".

--
Loïc

Avatar
Sylvain
Loïc Joly wrote on 13/07/2006 00:16:

Je n'ai jamais entendu James dire que c'était pareil. Juste que c'était
quand même un appel virtuel, mais qu'il fallait faire attention que le


..

type considéré était le type courant, et non le type final.


cette expression est pédagogiquement très compréhensible, je ne crois
pas l'avoir lu ainsi.

Ce qui a le mérite d'être vrai, alors que dire que dans un constructeur,
on considère le type statique, que le mécanisme de virtualité est
supprimé, est faux.


j'ai réfuté le terme "virtuel" car posé comme seule définition il
n'explique pas le scope de la résolution; bien sur que le mécanisme de
cette résolution existe ici et est unique <hs> ... mais on ne peut pas
me traiter de débile, incompétent et attendre rigueur en retour ...</hs>

Les exemples déjà cités le prouvent. Je trouve pour ma part l'exemple
plus parlant écrit ainsi :


oui, parlant - même limité à 3 étages, j'étais prêt à en mettre 15 ;)

Sylvain.

Avatar
kanze
Sylvain wrote:
kanze wrote on 12/07/2006 09:19:


[...]
comprendre qu'ici la réalisation de l'appel (pas sa logique
telle que définie par la norme) est différente *parce que l'on
est dans un constructeur*; nous rerépétez que ce serait tjrs
pareil se heurte à l'expérience et n'apprend rien au final.


Comprendre ce qui est faux, non, je ne sais pas faire.

Le problème, d'après mon expérience (concrète), c'est
plutôt avec la façon que fait Java, où on se trouve dans
une fonction membre sur un objet dont le constructeur n'a
pas encore été appelé. C'est une source d'erreurs
importante.




si tu écris des trus comme:

class Machin {
Truc truc;
Machin(){
maMethodeQuiTrucMuche();
truc = new Truc();
}
void maMethodeQuiTrucMuche(){
truc.muche();
}
};

t'as en effet un problème, mais pas de langage, de codage


C'est cependant un idiome courant dans les classes de Swing.
Vue que je n'ai pas la possibilité de les modifier, il faut que
je vis avec.

Je vois que tes connaissances de Java sont aussi faible que
celles du C++. Essaie de dériver d'une classe de Swing, par
exemple, et de supplanter certaines fonctions virtuelles. Tu
te rétrouves vite dans la fonction sans que le constructeur
a été appelé, et avec toutes les variables membre à null.


- je supplante tous les jours de nombreuses méthodes des
classes de javax.swing (ou d'autres packages) sans péter des
NullPointerException par erreur de codage,


C'est donc que tu ne fais que des choses simples, pas des
charpentes un peu élaboré. Java, c'est très bien pour des choses
simples.

Ou simplement que tu n'as pas rémarqué les exceptions, parce que
la boucle de base de Swing les bouffe.

- que /je/ sois "faible" parce que /tu/ génères de telles
erreurs est risible et pitoyable


Je ne savais pas que tu prenais la responsibilité des faiblesses
de Java. Et les erreurs, ici, sont bien dans Swing, et non dans
mon code. (Enfin, l'erreur de base, c'est bein dans la
conception du langage.)

- tes attaques ad homimem et répétitives sont hors chartre et
sans aucun intérêt, un jour peut être te lasseras-tu, pour ma
part et depuis le primaire j'ai toujours laissé autrui gagner
quand il s'agit de jouer au plus con.


Je n'attaque que quand tu dis une bêtise, parce qu'il y a la
risque que quelqu'un qui ne connaît pas le C++ te croît.

[...]
d'exception, on laisse tout comme il était, et on espère
pour le mieux. C'est quasiment impossible d'écrire un
programme « correct » dans de tels cas, mais qu'importe.


autre lacune Java ? je te rapelle qu'il y a un GC pour
récupérer "tout" et que tu as le droit de supplanter
finalize() si besoin.


La dernière fois que j'ai régardé, le GC ne supprimait pas des
fichiers temporaires, ne rétablissait pas des invariantes
internes du code, et ne fermait même pas les fichiers ouverts.
Les glaneurs de cellules, c'est très bien, au point où je m'en
sers regulièrement en C++ aussi (et que je travaille pour qu'il
devient une partie officielle du C++). Mais si c'est en général
la solution optimale pour la gestion de la mémoire, ce n'est
qu'une solution pour la gestion de la mémoire. Il ne traite pas
d'autres problèmes. Et la cohérence du programme ne dépend pas
que de la bonne gestion de la mémoire.

En C++, en cas d'exception, on appelle des destructeurs des
sous-objets déjà construits.


a) donc j'avais faux d'envisager un delete dans le
constructeur ... c'est pas delete, c'est
"delete-dit-par-James" ...


Encore une fois, tu n'as rien compris, probablement à cause des
faiblesses dans tes connaissances de C++. Quand il y une
exception dans un constructeur, le compilateur appelle des
destructeurs. Ce n'est pas un delete-dit-par-James, parce que 1)
ce n'est pas un delete, et 2) ça fait partie du langage, ce
n'est pas quelque chose dit par moi.

b) "on" appele même un delete sur les objects-membre alloués,
voui, voui et on s'emmerde encore à écrire proprement des
destructeurs ...


Je n'arrive toujours pas à saisir d'où tu trouves un delete.

faudrait arréter les amalgames, tu crois pas ? un handler
d'expection doit être codé correctement (re)"point".


D'où est-il question d'un handler d'exception ? On parle ici
des destructeurs. L'objet en question peut très bien être sur la
pile. Et le handler d'exception serait typiquement bien loin.
(En Java, à cause de l'absence des destructeurs, il faut
beaucoup plus de handlers d'exception. Que les programmeurs
moyens oublient assez souvents. En C++, en revanche, c'est en
général les destructeurs qui se chargent de faire le ménage.
Qu'on ne peut pas oublier.)

Qu'est-ce que les instances statiques aient à voir ici.


cela t'éviterait une NullPtrExc. quand tu oublies d'appeler un
cst.


Rien compris du rapport. C'est quoi un cst ?

-- et obtenir une NullPointerException est justement agréable,
Par rapport à quoi ?



relis ton post (un cran au-dessus)


Un NullPointerException (avalée par Swing) serait préférable à
un core dump ? Une vice cachée préférable à une erreur franche
lors des tests ?

Pas chez moi -- s'il y a une erreur dans mon code, je veux le
savoir tout de suite, avant que le code sort de chez moi.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34




Avatar
kanze
Sylvain wrote:
Loïc Joly wrote on 13/07/2006 00:16:

Je n'ai jamais entendu James dire que c'était pareil. Juste
que c'était quand même un appel virtuel, mais qu'il fallait
faire attention que le


..

type considéré était le type courant, et non le type final.


cette expression est pédagogiquement très compréhensible, je
ne crois pas l'avoir lu ainsi.


Et ça signifie quoi, dynamique, selon toi ?

Je sais que le français n'est pas ma langue maternelle, et qu'il
peut m'y arriver de ne pas m'exprimier aussi bien que je veux,
mais dans l'ensemble, j'ai l'impression qu'il n'y a que toi qui
n'avais pas compris.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
meow
Bon... Je reviens à mon post après renseigné sur les quelques infos
connexes que vos premières réponses m'on fait penser indispensable.
Je ne connaissais pas le mécanisme des vtables par exemple... Bon,
c'est parti pour le dépouillage :

1. si j'ai bien compris, la vtable n'est pas dans la norme C++, c'est
juste un mécanisme proposé pour la gestion des fonctions virtuelles,
et en l'occurrence, le mécanisme effectivement implanté par (tous?!)
les compilos.

+Arnaud
Vu autrement, lorsqu'un A est construit, il l'est avec une
vtable de A. Il n'a donc pas accès à la vtable de AA qui ne
sera « construite » que lors de la construction du AA (et donc, une f ois le A déjà
construit).


Si je comprends bien :
lors de l'execution du constructeur
AA(...):A(...){...}
on commence par construire A, et en particulier on initialise le
pointeur sur vtable de l'objet avec l'adresse de vtablle de la classe
A. Puis, lorsqu'on sort de là, on finit l'initialisation de l'objet en
écrasant cette valeur par celle de l'adresse de vtable de la classe
AA. Enfin on termine la construction en rentrant dans le bloc {...}.
Au passage, dans une arborescence à plusieurs étages, la vtable est
donc remplie et ecrasée autant de fois qu'on a d'ancètre.

+Kanze
Ce qu'il ne faut pas oublier, en revanche, c'est que pendant
l'exécution d'un constructeur (et d'un destructeur), le type
dynamique est celui du constructeur (ou destructeur), et non
celui qu'il sera par la suite


C'est dans la norme, ou bien c'est un corrollaire de l'implémentation
sus-décrite ?

+Kanze
En somme, rien de spécial : un comportement tout à fait normal,
celui auquel on s'attend pour peu qu'on y réflechit un peu, et
le seul possible qui est un peu cohérent. (On ne va quand même
pas choisir la fonction selon un état future incertain, non ?)


chais pas moi...
<mode zadig>On pourrait imaginer que lors de la construction d'un AA on
commence cash par remplir la vtable, puis lors de l'appel des
constructeurs des classes de base il y aurait un mécanisme qui
remarquerait que la vtable est déjà remplie et on y toucherait
pas</mode>
Mais si je comprends bien une assertion qui suit dans le fil de
discussion, j'ai une réponse qui laisse entendre que c'est ce que fait
java et que ça peut etre une mauvaise idée :

+Kanze
C'est une question de sécurité. Pour qu'un objet ait un type
dynamique T, il faut qu'on soit au moins entré dans le constructeur de T.


L'idée donc c'est que si je n'ai pas encore initialisé tout mon AA,
le fait d'utiliser des fonctions de AA pourrait m'emmener dans le mur
par exemple en jouant avec des pointeurs non initialisés... Du coup,
on préfère me filer des pointeurs vers des fonctions qui ne jouent
qu'avec des champs déjà initialisés. Mouais, c'est vrai que tout ça
se vaut :)

Du coup je me pose quand meme la question de l'ordre dans lequel les
fonctions sont appelée. Admettons que j'ai une arborescence A base de
AA base de AAA avec du virtuel dans tout ça et des constructeurs du
genre :
AAA():AA(){AAAinit();}
AA():A(){AAinit();}
A(){Ainit()}
lorsque j'écris
AAA aaa;
Que se passe t'il ? Logiquement je dirais :
On commence par appeller AA(), qui lui appelle A(), qui initialise par
défaut ses champs éventuels, puis initialise le pointeur de vtable de
mon objet aaa avec l'adresse de celle de A. Puis on entre dans le corps
du constructeur de A d'où appel à Ainit.
Hop, on sort, on écrase l'adresse de vtable avec celle de AA, on joue
avec AAinit(), on écrase l'adresse vtable avec celle de AAA, on
execute AAAinit() et c'est fini. J'ai bon ?

En hors sujet maintenant :
+Kanze
En C++, en cas d'exception, on appelle des destructeurs des sous-objets d éjà
construits.
C'est à dire qu'en remontant dans la pile des appels, tous les objets

"rencontrés" avant le "catch" sont détruits ?

Et pour revenir à mon problème concret :

+ Arnaud
Oui. À moins, par exemple, de passer par une factory amie qui
appellerait maMethode() juste après la construction.
Euh... si je comprends bien l'idée (je suis encore un peu noeud en

design) :
- mes classes A et AA auraient un constructeur en privé.
- J'aurai une classe F (Factory) amie de A qui construirait un A* sur
demande
F.giveMeA(A* a, kind), avec kind par exemple un enum {AA,AB,AC} pour
représenter les classes dérivées de A
- avec
giveMeA(A* a,kind){
switch (kind) {
case AA : a = new AA()
...
}
a->maFonction();
}

+ Kanze
Il y a des astuces avec des paramètres, aussi. Si, à la place de
lui passer un B, on lui passe un InitAAvecB (lié à une référence
à const), classe connue d'A (et dont A est ami) qui a un
Argh... désolé Kanze, il va falloir etre plus explicite sur ce coup

:D

+ Kanze
Sinon, il n'est pas rare chez moi d'utiliser le modèle de
stratégie, et de mettre le comportement dynamique dans une
classe de délégation. Qu'on construire complètement, évidemment,
avant d'entrer dans le corps du constructeur d'A, ou au moins
avant d'appeler la fonction en question.
woush... pas plus... Un exemple rapide ? si tu as le temps...


En tout cas, grand merci pour toutes ces réponses et pour le temps
passé.

Avatar
Arnaud Meurgues
meow wrote:

c'est parti pour le dépouillage :


[...]

Il me semble excellent.

Au passage, j'ai trouvé un extrait (je dirais bien « l'extrait », mais
je préfère rester prudent) de la norme qui explique le fonctionnement
des appels de fonctions dans les constructeurs :

«
12.7.3 Member functions, including virtual functions (10.3), can be
called during construction or destruction (12.6.2). When a virtual
function is called directly or indirectly from a constructor (including
from the meminitializer for a data member) or from a destructor, and the
object to which the call applies is the object under construction or
destruction, the function called is the one defined in the constructor
or destructor’s own class or in one of its bases, but not a function
overriding it in a class derived from the constructor or destructor’s
class, or overriding it in one of the other base classes of the most
derived object (1.8). If the virtual function call uses an explicit
class member access (5.2.5) and the objectexpression refers to the
object under construction or destruction but its type is neither the
constructor or destructor’s own class or one of its bases, the result of
the call is undefined.
»

Que se passe t'il ? Logiquement je dirais :
[...]

execute AAAinit() et c'est fini. J'ai bon ?


Ça me semble bon, oui.
Sachant que la gestion des vtables peut être compliqué par l'héritage
multiple (on hérite alors de deux vtables et « écraser » une vtable
devient quelque chose de moins évident).

En hors sujet maintenant :
+Kanze

En C++, en cas d'exception, on appelle des destructeurs des sous-objets déjà
construits.


C'est à dire qu'en remontant dans la pile des appels, tous les objets
"rencontrés" avant le "catch" sont détruits ?


Tout objet construit doit être détruit. Donc, pour tout constructeur qui
aura été entièrement exécuté, le destructeur correspondant doit être
appelé. Ce qui est assez logique car si un constructeur alloue une
resource, il faut bien libérer cette dernière.

Oui. À moins, par exemple, de passer par une factory amie qui
appellerait maMethode() juste après la construction.
Euh... si je comprends bien l'idée (je suis encore un peu noeud en

design) :
- mes classes A et AA auraient un constructeur en privé.
- J'aurai une classe F (Factory) amie de A qui construirait un A* sur
demande
F.giveMeA(A* a, kind), avec kind par exemple un enum {AA,AB,AC} pour
représenter les classes dérivées de A
- avec
giveMeA(A* a,kind){
switch (kind) {
case AA : a = new AA()
...
}
a->maFonction();
}


Oui, quelque chose comme ça. Après, ce peut être un argument kind ou
bien un autre moyen. On pourrait imaginer aussi par exemple une map avec
le nom de la classe qui renverrait la bonne factory.

--
Arnaud


Avatar
meow
Au passage, j'ai trouvé un extrait (je dirais bien « l'extrait », m ais
OK, merci.


Sachant que la gestion des vtables peut être compliqué par l'hérita ge
multiple (on hérite alors de deux vtables et « écraser » une vtab le
devient quelque chose de moins évident).
Certes... Tu as des actions chez Aspro ? ;)


Avatar
kanze
meow wrote:
Bon... Je reviens à mon post après renseigné sur les quelques
infos connexes que vos premières réponses m'on fait penser
indispensable. Je ne connaissais pas le mécanisme des vtables
par exemple... Bon, c'est parti pour le dépouillage :

1. si j'ai bien compris, la vtable n'est pas dans la norme
C++, c'est juste un mécanisme proposé pour la gestion des
fonctions virtuelles, et en l'occurrence, le mécanisme
effectivement implanté par (tous?!) les compilos.


Tout à fait.

En fait, le vtable proprement dit est un objet statique, un
tableau avec les informations dont a besoin le compilateur pour
rétrouver la bonne fonction (et d'autres choses). Dans chaque
instance de l'objet, il y a un vptr, c-à-d un pointeur à cet
objet statique.

+Arnaud
Vu autrement, lorsqu'un A est construit, il l'est avec une
vtable de A. Il n'a donc pas accès à la vtable de AA qui ne
sera « construite » que lors de la construction du AA (et
donc, une fois le A déjà construit).


Si je comprends bien :
lors de l'execution du constructeur
AA(...):A(...){...}

on commence par construire A, et en particulier on initialise
le pointeur sur vtable de l'objet avec l'adresse de vtablle de
la classe A. Puis, lorsqu'on sort de là, on finit
l'initialisation de l'objet en écrasant cette valeur par celle
de l'adresse de vtable de la classe AA. Enfin on termine la
construction en rentrant dans le bloc {...}. Au passage, dans
une arborescence à plusieurs étages, la vtable est donc
remplie et ecrasée autant de fois qu'on a d'ancètre.


Tout à fait.

On pourrait voir bien ce qui ce passe avec une fonction du
genre :

void
displayType( Base const* p )
{
std::cout << typeid( *p ).name() << std::endl ;
}

en en appelant cette fonction dans tous les constructeurs, avec
le pointeur this (qui serait converti en Base const* avant
l'appel).

+Kanze
Ce qu'il ne faut pas oublier, en revanche, c'est que pendant
l'exécution d'un constructeur (et d'un destructeur), le type
dynamique est celui du constructeur (ou destructeur), et non
celui qu'il sera par la suite


C'est dans la norme, ou bien c'est un corrollaire de
l'implémentation sus-décrite ?


C'est dans la norme. C'est l'implémentation qui dérive de la
norme.

+Kanze
En somme, rien de spécial : un comportement tout à fait normal,
celui auquel on s'attend pour peu qu'on y réflechit un peu, et
le seul possible qui est un peu cohérent. (On ne va quand même
pas choisir la fonction selon un état future incertain, non ?)


chais pas moi...
<mode zadig>On pourrait imaginer que lors de la construction d'un AA on
commence cash par remplir la vtable, puis lors de l'appel des
constructeurs des classes de base il y aurait un mécanisme qui
remarquerait que la vtable est déjà remplie et on y toucherait
pas</mode>
Mais si je comprends bien une assertion qui suit dans le fil de
discussion, j'ai une réponse qui laisse entendre que c'est ce que fait
java et que ça peut etre une mauvaise idée :


On pourrait imaginer beaucoup de chose:-). De différents
langages ont choisi de différentes solutions.

+Kanze
C'est une question de sécurité. Pour qu'un objet ait un type
dynamique T, il faut qu'on soit au moins entré dans le
constructeur de T.


L'idée donc c'est que si je n'ai pas encore initialisé tout
mon AA, le fait d'utiliser des fonctions de AA pourrait
m'emmener dans le mur par exemple en jouant avec des pointeurs
non initialisés... Du coup, on préfère me filer des pointeurs
vers des fonctions qui ne jouent qu'avec des champs déjà
initialisés. Mouais, c'est vrai que tout ça se vaut :)


L'idée de base, c'est que tu n'as pas encore commencé à
initialiser ton AA. On pourrait dire que ton AA a plusieurs
états :

-- Au départ, c'est de la mémoire brute. Prèsque tout ce que tu
peux en faire serait un comportement indéfini. (Dans le cas
d'un objet appelé dynamiquement, ou d'un objet sur la pile,
je vois mal comment tu y accéderais pour y faire quelque
chose. Dans le cas des objets statiques, en revanche... Tu
m'entendras parler parfois des questions de l'ordre
d'initialisation. C'est que le constructeur d'un objet
statique peut essayer d'utiliser un autre objet statique,
dont le constructeur n'a pas encore été appelé.)

-- On commence par la construction des classes de base. Pendant
ce temps-là, l'objet n'est encore en rien un AA. Typeid dit
que ce n'est pas un AA, les appels des fonctions
virtuelles ne peuvent se résoudre à des fonctions de AA, et
un dynamic_cast à AA* ou à AA& échouera.

-- Une fois la construction de toutes les classes de base
finies, on entre dans le constructeur de la classe finale. À
partir de ce moment, il se comporte comme un AA, sans
réelement en être un complètement. En particulier, typeid
trouve que c'est un AA, les appels aux fonctions
virtuelles se résoudent à AA, et dynamic_cast dit que c'est
un AA.

Note bien qu'à cet instant, l'initialisation de AA n'est pas
encore fini, et que tu peux y avoir encore des problèmes.
Mais tout appel à une fonction virtuelle, toute utilisation
de typeid ou de dynamic_cast ne peut venir que de quelque
chose que tu fais dans ton code. Tu peux donc faire des
précautions. Tandis que si c'était le constructeur d'une
classe de base qui appelait ta fonction, tu n'as aucune
chance ; il n'y a pas une ligne de code à toi qui s'est
exécutée sur l'objet.

(Je signalerais en passant que la situation est légèrement
différente en Java. Ce qu'on appelle l'« initialisation à
zéro » en C++, Java l'applique sur toute l'instance de la
classe avant de commencer la construction, et non seulement
sur des instances statiques. Je pourrais donc en Java mettre
un booléen, constructorExecuted, et le tester dans chaque
fonction. Du coup, si la décision en Java est regrettable,
elle n'est pas tout à fait le désastre qu'on serait en C++,
où tu aurais des valeurs non-initialisées.)

-- Finalement, ce n'est qu'en sortie du constructeur de la
classe la plus dérivée que l'objet devient un AA pour le
bon. En cas d'exception, par exemple, le compilateur appelle
les destructeurs pour les sous-objets complètement
construit ; c-à-d ceux dont on a sorti normalement du
constructeur. De même, considère un objet local statique :

void
f()
{
static AA a ;
}

On apprend que cet objet sera construit une seule fois, la
première fois que le flux d'exécution rencontre la
définition. Mais la vraie règle est légèrement plus
subtile : l'objet serait construit chaque fois que le flux
d'exécution pas par la définition et que l'objet n'est pas
encore construit. Si la première fois, la construction de a
termine par une exception, l'objet n'est pas construit (et
les destructeurs des sous-objets complètement construits
seront appelé lors du nettoyage de la pile) ; on appelera
le constructeur encore une deuxième fois la prochaine fois
qu'on appelle f(). (Et il y a intérêt à ce que le
constructeur de AA n'appelle pas f(), parce que ça donnerait
une récursion sans fin.)

Du coup je me pose quand meme la question de l'ordre dans
lequel les fonctions sont appelée. Admettons que j'ai une
arborescence A base de AA base de AAA avec du virtuel dans
tout ça et des constructeurs du genre :
AAA():AA(){AAAinit();}
AA():A(){AAinit();}
A(){Ainit()}
lorsque j'écris
AAA aaa;
Que se passe t'il ? Logiquement je dirais :

On commence par appeller AA(), qui lui appelle A(), qui
initialise par défaut ses champs éventuels, puis initialise le
pointeur de vtable de mon objet aaa avec l'adresse de celle de
A. Puis on entre dans le corps du constructeur de A d'où appel
à Ainit. Hop, on sort, on écrase l'adresse de vtable avec
celle de AA, on joue avec AAinit(), on écrase l'adresse vtable
avec celle de AAA, on execute AAAinit() et c'est fini. J'ai
bon ?


Tout à fait. La règle de base, c'est d'initialiser (dans
l'ordre) :

-- les classes de base virtuelles, dans l'ordre qu'elles
apparaissent pour la première fois lors d'une visite en
profondeur d'abord, de gauche à droite, puis

-- les classes de base directe (qui eux, suivront les même
règles récursivement en ce qui concerne leurs bases non
virtuelles, mais qui n'initialiseront pas les bases
virtuelles, parce qu'elles ont déjà été initialisées), de
gauche à droite, dans l'ordre qu'elles apparaissent dans la
définition de la classe (et NON dans l'ordre des
initialisations dans la liste d'initialisation du
constructeur), puis

-- les membres (non statique, évidemment), dans l'ordre de leur
définition dans la définition de la classe de base.

En hors sujet maintenant :
+Kanze
En C++, en cas d'exception, on appelle des destructeurs des
sous-objets déjà construits.


C'est à dire qu'en remontant dans la pile des appels, tous les
objets "rencontrés" avant le "catch" sont détruits ?


Et encore avant. En prenant ton exemple avec AA qui dérive de A,
si le constructeur de AA sort par une exception, on appelle le
destructeur de A sur l'objet.

C'est un concepte très important en C++, qui sert énormement
pour garantir la bonne fonctionnement en cas d'exception. Si tu
régardes les implémentations de std::vector, par exemple, il y a
prèsque toujours une classe de base qui contient les données
réeles, et dont le constructeur créer un vecteur vide. Ensuite,
dans le constructeur de std::vector même, on remplira le
vecteur (dont le compteur des éléments valides se trouve dans la
classe de base). Si, lors du remplissage, un des constructeurs
de copie lève une exception, le destructeur de la classe de base
sera appelé, et il détruira tous les éléments déjà construits.

Et pour revenir à mon problème concret :

+ Arnaud
Oui. À moins, par exemple, de passer par une factory amie qui
appellerait maMethode() juste après la construction.
Euh... si je comprends bien l'idée (je suis encore un peu noeud en

design) :
- mes classes A et AA auraient un constructeur en privé.


Il vaut mieux que classe A ait un constructeur protégé, quand
même.

- J'aurai une classe F (Factory) amie de A qui construirait un A* sur
demande
F.giveMeA(A* a, kind), avec kind par exemple un enum {AA,AB,AC} pour
représenter les classes dérivées de A


C'est une possibilité. Une autre, c'est que tu as une fonction
statique membre de la classe A qui renvoie le bon objet.

Note que si les constructeurs des classes dérivées sont privés,
il faut que la classe de base (ou la classe usine) en soit ami.

Typiquement, cette solution s'applique à une hièrarchie fermée,
où le choix des classes dérivées dépend plus ou moins
directement des paramètres -- l'utilisateur ne passe pas un
identificateur de classe directement, mais la fonction ou la
classe usine l'en deduit indirectement.

- avec
giveMeA(A* a,kind){
switch (kind) {
case AA : a = new AA()
...
}
a->maFonction();
}


Peut-être plutôt un std::map< Kind, Usine* >.

+ Kanze
Il y a des astuces avec des paramètres, aussi. Si, à la
place de lui passer un B, on lui passe un InitAAvecB (lié à
une référence à const), classe connue d'A (et dont A est
ami) qui a un


Argh... désolé Kanze, il va falloir etre plus explicite sur ce
coup :D


C'est loin d'être trival, mais, supposons que ta classe A (et
donc AA) ait besoin d'un seul paramètre, B* :

class InitAWithB
{
public:
InitAWithB( B* arg )
: myOwner( NULL )
, itsArg( arg )
{
}

~InitAWithB()
{
if ( myOwner != NULL ) {
myOwner->maMethod() ;
}
}
private:
mutable A* myOwner ;
B* itsArg ;

friend class A ;
} ;

class A
{
public:
A( InitAWithB const& arg )
: myB( arg->itsArg )
{
arg->myOwner = this ;
}

virtual void maMethod() = 0 ;
} ;

class AA : public A
{
public:
AA( InitAWithB const& arg )
: A( arg )
{
}
// ...
} ;

L'utilisateur écrit « new AA( &someB ) ». Il n'y a pas de
constructeur d'AA qui prend un B*, mais il y a une conversion
implicite de B* en InitAWithB. Le compilateur effectue donc la
conversion (qui crée un temporaire), et il appelle AA avec ce
temporaire. À la fin de l'expression complète, et donc, après
qu'on est sorti du constructeur de AA, le destructeur du
temporaire est appelé, et c'est lui qui appelle maMethod(). Sur
un objet qui est réelement un AA.

Note que cette technique ne marche pas si tu veux utiliser le AA
aussi comme temporaire. Si tu écris quelque chose du genre :
AA( &someB ).foo() ;
par exemple, AA::foo() serait bien appelée avant
AA::maMethod -- pire, AA (et donc A) serait détruit avant
l'appel de AA:maMethod(). Selon la finalité de la classe, ça
peut être un problème ou non.

+ Kanze
Sinon, il n'est pas rare chez moi d'utiliser le modèle de
stratégie, et de mettre le comportement dynamique dans une
classe de délégation. Qu'on construire complètement,
évidemment, avant d'entrer dans le corps du constructeur
d'A, ou au moins avant d'appeler la fonction en question.


woush... pas plus... Un exemple rapide ? si tu as le temps...


Pour la stratégie : d'abord, je te conseillerai le livre des
modèles de conception. Mais en gros : ce que tu es en train de
faire s'appelle le modèle du template (à ne pas confondre avec
les templates du C++), c-à-d que tu définis une classe qui a un
comportement customisable, et que la customisation se passe par
l'appel des fonctions virtuelles supplantées dans une classe
dérivée. C'est un peu une exception à la règle qui veut que la
plupart des classes de base n'ont aucun comportement en soi,
qu'elles sont des « interfaces ». Le seul problème, c'est que
ta customisation a lieu dans le constructeur, et à ce moment-là,
il n'y a pas de classe dérivée.

Dans le modèle de stratégie, la customisation a lieu dans un
objet à part, le délégué. (Je ne suis pas sûr que ce soit le nom
officiel. J'utilisais le modèle bien avant d'avoir lu « Design
Patterns », et je parlais alors de la délégation, parce que je
ne savais pas que le modèle s'appelait stratégie.) Pour un
exemple tiré du code de production, tu peux régarder à
http://kanze.james.neuf.fr/code/Util/Text/FieldArray/index.html ;
il y a une implémentation complète d'une classe de base et trois
classes dérivées qui s'en servent. (Les .hh sont dans le
sous-répertoire gb. Pour des renseignements plus général sur le
code là, ainsi que des liens vers la documentation, etc. :
http://kanze.james.neuf.fr/code-fr.html. Et aussi, si le gros du
code vient en effet du code de production, l'utilisation d'un
décorateur sur la stratégie est un peu expérimentale.)

Le modèle de stratégie est en général plus souple que le modèle
template. En revanche, il exige la création d'un objet de plus,
avec potentiellement des problèmes de sa durée de vie.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


1 2