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
Arnaud Meurgues
meow wrote:

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

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

Ce qui ne marche pas parce que le linker semble chercher
A::maMethode()...


Oui. Il n'y a pas d'appel virtuel possible dans un constructeur. Dans le
cas ci-contre, lorsqu'on construit un AA, on commence par construire un
A. Donc, lorsque le constructeur de A est appelé, il ne sait pas que
l'objet est un AA. Il sait juste que c'est un A et ne peut donc faire de
résolution dynamique à ce moment là.

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).

Suis-je condamné à descendre l'appel à maMethode dans le corps du
constructeur de AA et des autres classes filles ? Une autre idée ?


Oui. À moins, par exemple, de passer par une factory amie qui
appellerait maMethode() juste après la construction.

--
Arnaud

Avatar
kanze
Arnaud Meurgues wrote:
meow wrote:

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

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

Ce qui ne marche pas parce que le linker semble chercher
A::maMethode()...


Oui. Il n'y a pas d'appel virtuel possible dans un
constructeur.


Bien sûr que si. L'appel virtuel marche de la même façon que
n'importe où ailleur. La fonction à appeler est choisie en
fonction du type dynamique de l'objet.

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. Donc, ici, lors de l'appel à
maMethode() dans le constructeur d'A, le type dynamique est A,
et la résolution de l'appel serait A::maMethode. Et que quand la
résolution de l'appel dynamique se résoud à une fonction
virtuelle pûre (comme c'est le cas ici), on a un comportement
indéfini, que la fonction soit implémentée ou non. La plupart
des compilateurs abortera le programme, avec un message
d'erreur, mais on ne peut pas y compter.

Dans le cas ci-contre, lorsqu'on construit un AA, on commence
par construire un A. Donc, lorsque le constructeur de A est
appelé, il ne sait pas que l'objet est un AA.


Oui, mais quand j'appelle une fonction à travers un A*, on ne
sait pas non plus qu'on a un objet de type AA.

Il sait juste que c'est un A et ne peut donc faire de
résolution dynamique à ce moment là.


Mais il peut le faire, et le cas échéant, il le fait.

C'est plutôt l'inverse, je crois. Ici, lors de l'appelle de
maMethode dans A::A, le compilateur sait bien que le type
dynamique est A, parce que le type dynamique d'un objet lors de
l'exécution du constructeur est le type du constructeur, par
définition. Au moins d'être particulièrement abruti, il doit au
moins avertir.

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).


C'est effectivement ce qui se passe derrière la scène.

Suis-je condamné à descendre l'appel à maMethode dans le
corps du constructeur de AA et des autres classes filles ?
Une autre idée ?


Oui. À moins, par exemple, de passer par une factory amie qui
appellerait maMethode() juste après la construction.


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
constructeur qui permet la conversion implicite du paramètre B&
que passe l'utilisateur. Du coup, il y a la conversion
implicite, ce qui crée un temporaire. A::A, en tant qu'ami,
peche le B qu'il lui faut du InitAAvecB, et y met son pointeur
this. Dans le destructeur de InitAAvecB, on appelle la fonction
voulue de A. Et puisque le destructeur ne sera appelé qu'à la
fin de l'expression, l'objet en question a eu le temps de
définir un AA.

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.

Ni l'une ni l'autre de ces solutions ne s'appliquent
automatiquement partout, mais l'une ou l'autre peut parfois
résoudre le problème.

--
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 11/07/2006 19:40:

Oui. Il n'y a pas d'appel virtuel possible dans un
constructeur.


Bien sûr que si. L'appel virtuel marche de la même façon que
n'importe où ailleur. La fonction à appeler est choisie en
fonction du type dynamique de l'objet.


non, il n'est pas "virtuel", pour la bonne raison exposé par Arnaud.
le fait que dans une arborescense à 15 étages, un appel fait depuis le
7ième ira en effet chercher la méthode du RDC au 7ième n'est pas ce
qu'attends le PO (ni toute "personne normale"); tu dois confondre avec
un langage à "grandes faiblesses" qui lui gère cela très bien.

Oui, mais quand j'appelle une fonction à travers un A*, on ne
sait pas non plus qu'on a un objet de type AA.


parce que tu trouves des A* tombé du ciel (envoyé par un code
auto-généré que tu ne peux pas connaitre) ou que tu ne sais plus faire
un dynamic_cast (si vraiment tu avais besoin de lever un doute) ?

C'est plutôt l'inverse, je crois. Ici, lors de l'appelle de
maMethode dans A::A, le compilateur sait bien que le type
dynamique est A, parce que le type dynamique d'un objet lors de
l'exécution du constructeur est le type du constructeur, par
définition. [...]


et c'est ce que l'on "reproche" ! (comme le fait de ne pas pouvoir
appeller un constructeur de la même classe depuis un constructeur).

tu réponds que l'on /peut/ faire intervenir les pattern design X ou Y,
le fait est que l'on /doit/ complexifier l'écriture via un tel design.

Sylvain.


Avatar
James Kanze
Sylvain wrote:
kanze wrote on 11/07/2006 19:40:


Oui. Il n'y a pas d'appel virtuel possible dans un
constructeur.




Bien sûr que si. L'appel virtuel marche de la même façon que
n'importe où ailleur. La fonction à appeler est choisie en
fonction du type dynamique de l'objet.



non, il n'est pas "virtuel", pour la bonne raison exposé par Arnaud.


Ça serait mieux que tu apprends un peu de C++ avant de t'avancer. Si la
fonction est déclarée virtuelle, la résolution dynamique s'applique. Que
la fonction soit appelée dans le constructeur, ou ailleurs. Selon la
norme, et avec tous les compilateurs que je connais. Tu n'as donc qu'à
l'essayer.

le fait que dans une arborescense à 15 étages, un appel fait depuis le
7ième ira en effet chercher la méthode du RDC au 7ième n'est pas ce
qu'attends le PO (ni toute "personne normale"); tu dois confondre avec
un langage à "grandes faiblesses" qui lui gère cela très bien.


Le niveau d'arborescences n'a rien à voir dans l'affaire. Le fait reste
que la résolution dépend du type dynamique, et non du type statique de
l'expression de l'appel.

C'est plutôt l'inverse, je crois. Ici, lors de l'appelle de maMethode
dans A::A, le compilateur sait bien que le type dynamique est A,
parce que le type dynamique d'un objet lors de l'exécution du
constructeur est le type du constructeur, par définition. [...]



et c'est ce que l'on "reproche" !


Jusqu'ici, je n'ai pas entendu de reproches. 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.

(comme le fait de ne pas pouvoir appeller un constructeur de la même
classe depuis un constructeur).


On est en train de l'ajouter. Mais la sémantique n'est pas forcément
évidente. (Encore, c'est plus facile en Java à cause de l'absence des
destructeurs. Ce qui a, en revanche, d'autres désavantages.)

tu réponds que l'on /peut/ faire intervenir les pattern design X ou Y,
le fait est que l'on /doit/ complexifier l'écriture via un tel design.


C'est le prix à payer pour la correction. C'est pareil dans d'autres
langages, avec la différence qu'en C++, tu as un comportement indéfini
(mais un core dump avec un bon compilateur), tandis qu'en Java, tu as
une NullPointerException, sinon un résutlat erroné.

C'est un des cas où le C++ fait mieux que le Java, indiscutablement,
pour celui qui tient à du code robuste.

--
James Kanze
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
James Kanze wrote on 11/07/2006 23:50:

Ça serait mieux que tu apprends un peu de C++ avant de t'avancer. Si la
fonction est déclarée virtuelle, la résolution dynamique s'applique.


avec un périmêtre constraint par le lieu d'appel (ici un constructeur
parent particulier).

Que la fonction soit appelée dans le constructeur, ou ailleurs.


struct A {
A() { init(); }
void doInit() { init(); }
virtual void init() {}
};
struct B : A {
B() : A() {}
void init() {}
};

B b; // invoque A::init();
b.doInit(); // invoque B::init();

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

et c'est ce que l'on "reproche" !


Jusqu'ici, je n'ai pas entendu de reproches.


change par "ce que l'on regrette" si cela t'aide à adresser cette
"particularité".

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 appelles les fonction membres sans faire un new sur l'instance
avant, oui c'est génant, mais c'est une erreur de débutant ...

(comme le fait de ne pas pouvoir appeller un constructeur de la même
classe depuis un constructeur).


On est en train de l'ajouter. Mais la sémantique n'est pas forcément
évidente.


ahhh, voila une bonne nouvelle !

(Encore, c'est plus facile en Java à cause de l'absence des
destructeurs. Ce qui a, en revanche, d'autres désavantages.)


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

C'est le prix à payer pour la correction. C'est pareil dans d'autres
langages, avec la différence qu'en C++, tu as un comportement indéfini
(mais un core dump avec un bon compilateur), tandis qu'en Java, tu as
une NullPointerException, sinon un résutlat erroné.


la comparaison n'a aucun sens - les instances statiques n'existent pas
en Java "point" -- et obtenir une NullPointerException est justement
agréable, rappelle-nous le compotement défini par le norme pour le code
C++ suivant :

UneClasseValide* ptr;
ptr->UneMethodeValide();

C'est un des cas où le C++ fait mieux que le Java, indiscutablement,
pour celui qui tient à du code robuste.


rien à voir; "pour celui qui ne veux gérer que des instances statiques"
si tu le souhaites.

Sylvain.


Avatar
Falk Tannhäuser
Sylvain wrote:
kanze wrote on 11/07/2006 19:40:
L'appel virtuel marche de la même façon que
n'importe où ailleur. La fonction à appeler est choisie en
fonction du type dynamique de l'objet.
non, il n'est pas "virtuel", pour la bonne raison exposé par Arnaud.



#include <iostream>
#include <ostream>

struct A
{
virtual void hello() const { std::cout << "Aahn"; }
};

void foo(A const& a) { a.hello(); }

struct B : public A
{
B() { foo(*this); }
virtual void hello() const { std::cout << "Behn"; }
};

int main()
{
B b;
return 0;
}

L'appel à hello() dans foo() est bien résolu de façon virtuelle, le
type dynamique de l'objet en question étant B au moment de l'appel.

Falk


Avatar
kanze
Sylvain wrote:
James Kanze wrote on 11/07/2006 23:50:

Ça serait mieux que tu apprends un peu de C++ avant de
t'avancer. Si la fonction est déclarée virtuelle, la
résolution dynamique s'applique.


avec un périmêtre constraint par le lieu d'appel (ici un
constructeur parent particulier).


Ce qui signifie quoi, exactement ? La norme est claire ; la
récherche du nom (de la fonction) et la résolution du surcharge
se font selon le type statique de l'expression (de même que la
vérification d'accessibilité). Ensuite, si la fonction est
déclarée virtuelle, la résolution dynamique s'applique. Il n'y a
pas de règle spéciale en ce qui concerne un périmêtre ou quoique
ce soit. La notion de « périmêtre constraint » ne fait pas
partie du C++ (et du coup, je ne sais pas ce que tu veux dire
par cette expression).

Que la fonction soit appelée dans le constructeur, ou
ailleurs.


struct A {
A() { init(); }
void doInit() { init(); }
virtual void init() {}
};
struct B : A {
B() : A() {}
void init() {}
};

B b; // invoque A::init();
b.doInit(); // invoque B::init();

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. L'appel de la fonction se passe exactement pareil
dans le constructeur qu'ailleurs. Si la fonction est virtuelle,
la résolution est dynamique, et prend en compte le type
dynamique de l'objet. Dans la mesure où le type dynamique varie
en cour de route, la fonction appelée va varier aussi.

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 ?)

et c'est ce que l'on "reproche" !


Jusqu'ici, je n'ai pas entendu de reproches.


change par "ce que l'on regrette" si cela t'aide à adresser
cette "particularité".


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 ?

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 appelles les fonction membres sans faire un new sur
l'instance avant, oui c'est génant, mais c'est une erreur de
débutant ...


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.

(Encore, c'est plus facile en Java à cause de l'absence des
destructeurs. Ce qui a, en revanche, d'autres désavantages.)


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
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. En C++, en cas
d'exception, on appelle des destructeurs des sous-objets déjà
construits.

Pour cela, évidemment, il faut savoir quand on considère le
sous-objet « construit ». Actuellement, c'est quand on sort du
constructeur. Mais si on appelle plusieurs constructeurs ?

C'est le prix à payer pour la correction. C'est pareil dans
d'autres langages, avec la différence qu'en C++, tu as un
comportement indéfini (mais un core dump avec un bon
compilateur), tandis qu'en Java, tu as une
NullPointerException, sinon un résutlat erroné.


la comparaison n'a aucun sens - les instances statiques
n'existent pas en Java "point"


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

-- et obtenir une NullPointerException est justement agréable,


Par rapport à quoi ?

rappelle-nous le compotement défini par le norme pour le code
C++ suivant :

UneClasseValide* ptr;
ptr->UneMethodeValide();


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).

C'est un des cas où le C++ fait mieux que le Java,
indiscutablement, pour celui qui tient à du code robuste.


rien à voir; "pour celui qui ne veux gérer que des instances
statiques" si tu le souhaites.


Je ne vois toujours pas ce que les instances statiques a à voir
là-dedans. On parle de l'héritage, et le fait d'appeler des
fonctions sur des objets non-construits. Ce que le C++ ne
supporte pas, tandis que le Java, si.

--
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
Arnaud Meurgues
kanze wrote:

Bien sûr que si. L'appel virtuel marche de la même façon que
n'importe où ailleur. La fonction à appeler est choisie en
fonction du type dynamique de l'objet.


Ciel ! Effectivement. Tout se passe comme s'il ne l'était pas mais il l'est.

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.


En fait, la résolution est bien dynamique dans le sens qu'elle passe par
la vtable, mais c'est la vtable de l'objet en train d'être construit et
non celle de l'objet final. C'est bien ça ?

Oui, mais quand j'appelle une fonction à travers un A*, on ne
sait pas non plus qu'on a un objet de type AA.


Certes, mais la vtable est correcte à ce moment là (en considérant une
implémentation par vtable. Il me semble qu'il y a d'autres possibilités,
mais je ne les ai jamais rencontrées).

--
Arnaud

Avatar
kanze
Arnaud Meurgues wrote:
kanze wrote:

Bien sûr que si. L'appel virtuel marche de la même façon que
n'importe où ailleur. La fonction à appeler est choisie en
fonction du type dynamique de l'objet.


Ciel ! Effectivement. Tout se passe comme s'il ne l'était pas
mais il l'est.


Tout se passe comme s'il ne l'était pas dans des cas assez
simples. Mais c'est facile à construire des cas où la virtualité
se voit :

struct Base
{
virtual f() { std::cout << "in Base" << std::endl ; }
} ;

void
f( Base const* p )
{
p->f() ;
}

struct Intermediary : Base
{
virtual f() { std::cout << "in Intermediary" << std::endl ; }
} ;

struct Derived : Intermediary
{
Derived()
{
f( this ) ;
}
} ;

int
main()
{
Derived d ;
return 0 ;
}

On voit bien que l'appel dans f exerce la virtualité.

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.


En fait, la résolution est bien dynamique dans le sens qu'elle
passe par la vtable, mais c'est la vtable de l'objet en train
d'être construit et non celle de l'objet final. C'est bien
ça ?


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. Si tu régardes le code généré, tu verras une
initialisation du vptr pour chaque niveau. (Au moins si les
constructeurs ne sont pas inline. Si le compilateur voit que le
constructeur n'a pas besoin du vptr, il ne l'initialise pas
forcement.)

Oui, mais quand j'appelle une fonction à travers un A*, on
ne sait pas non plus qu'on a un objet de type AA.


Certes, mais la vtable est correcte à ce moment là (en
considérant une implémentation par vtable. Il me semble qu'il
y a d'autres possibilités, mais je ne les ai jamais
rencontrées).


Tout à fait. La question ici est plutôt, qu'est-ce qu'on entend
par vtable correcte ? Ou autrement dit : qu'est-ce que c'est
le type dynamique de l'objet à un moment donné ?

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. (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é.)

--
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
Arnaud Meurgues
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.

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) ?

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).
--
Arnaud

1 2