Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Probleme de vitualisation

22 réponses
Avatar
Dominique Vaufreydaz
Bonjour,

j'ai ecrit une classe thread (suis pas le premier, hein ;-P) qui en fait contient une
fonction Run virtuelle pure qui doit etre redefini dans une classe dérivée. Rien
de vraiment difficile. Cette classe thread contient une fonction StartThread()

J'ai rajouté un booléen sur le constructeur du Thread. Si on lui passe true, il
demarre tout seul en appelant StartThread. Je me suis dit que de toute facon,
je ne peux pas instancier la classe Thread (car abstraite du fait du virtual ... = 0).

Le probleme qui se pose (sous g++ uniquement avec comme flags de compilation
-Werror -Wall -pedantic -std=c++98 -fPIC) c'est que je me retrouve avec
des erreurs (et pas a chacune des executions) du genre.
> pure virtual method called
> terminate called without an active exception
> Abort

A priori, pas de debordement ailleurs (je verifie encore) qui justifierait l'ecrasement
de la table des fonctions virtuelles. Des idées ? Notons que le meme mecanisme fonctionne
a merveille sous Visual Studio 2005. Peut-etre un flag a rajouter à g++ ?

Merci d'avance. Doms.

10 réponses

1 2 3
Avatar
Laurent Deniau
James Kanze wrote:
On Apr 4, 7:46 am, "Dominique Vaufreydaz" wrote:
Je me suis dit que de toute facon, je ne peux
pas instancier la classe Thread (car abstraite du fait du
virtual ... = 0).

Le probleme qui se pose (sous g++ uniquement avec comme flags de compilation
-Werror -Wall -pedantic -std=c++98 -fPIC) c'est que je me retrouve avec
des erreurs (et pas a chacune des executions) du genre.

pure virtual method called
terminate called without an active exception
Abort


A priori, pas de debordement ailleurs (je verifie encore)
qui justifierait l'ecrasement de la table des fonctions
virtuelles. Des idées ?


C'est un comportement indéfini. Lors de la construction de
Thread, le type dynamique de l'objet est Thread.


De maniere generale, les objets polymorphiques deviennent monomorphiques
dans leurs constructeurs et destructeurs.

Ce qui veut
dire que la résolution dynamique trouve la version de la
fonction dans Thread. Et la norme dit que si la résolution
dynamique trouve une fonction virtuelle pûre, c'est un
comportement indéfini (même si la fonction a une définition).

Notons que le meme mecanisme
fonctionne a merveille sous Visual Studio 2005.


C'est probablement le résultat d'une optimisation. Le
compilateur, sachant que la fonction est pûre virtuelle, et ne
peut pas être appelée par la résolution dynamique dans le
constructeur, se passe d'initialiser le vtable pour la classe de
base, mais l'initialise immédiatement pour la classe dérivée.


La, je ne te suis pas. Quand tu parles "d'initialiser la vtable pour la
classe de base", tu parles de la table elle-meme ou du pointeur __vtbl
dans l'objet? Pour moi, la seule explication de ce comportement, c'est
l'absence d'ajustement de __vtbl dans le constructeur qui fait que
l'objet est du type Derivee au lieu d'etre du type Base et donc il
continue de voir les methodes de Derivee. C'est donc un bug du compilateur.

a+, ld.



Avatar
James Kanze
On Apr 5, 12:31 pm, Laurent Deniau wrote:
James Kanze wrote:
On Apr 4, 7:46 am, "Dominique Vaufreydaz" wrote:
Je me suis dit que de toute facon, je ne peux
pas instancier la classe Thread (car abstraite du fait du
virtual ... = 0).

Le probleme qui se pose (sous g++ uniquement avec comme
flags de compilation
-Werror -Wall -pedantic -std=c++98 -fPIC) c'est que je me retrou ve avec
des erreurs (et pas a chacune des executions) du genre.

pure virtual method called
terminate called without an active exception
Abort


A priori, pas de debordement ailleurs (je verifie encore)
qui justifierait l'ecrasement de la table des fonctions
virtuelles. Des idées ?


C'est un comportement indéfini. Lors de la construction de
Thread, le type dynamique de l'objet est Thread.


De maniere generale, les objets polymorphiques deviennent monomorphiques
dans leurs constructeurs et destructeurs.


Pas vraiment. Qu'on ait plusieurs niveaux d'héritage, et que les
constructeurs appellent des fonctions externes avec this comme
paramètre, et la polymorphisme devient évident. Le type
dynamique de l'objet devient celui du constructeur qui
s'exécute. Dans le constructeur même, c'est évident, c'est aussi
le type statique de this, et quand le type statique et le type
dynamique s'accorde, et que le compilateur en plus le sait...

Mais considère :

class A
{
public
virtual ~A() {}
virtual void f() { std::cout << "coucou d'A" << std::endl ; }
} ;

void g( A* pa )
{
pa->f() ;
}

class B : public A
{
public:
B() { g( this ) ; }
virtual void f() { std::cout << "coucou d'B" << std::endl ; }
} ;

class C : public B
{
public:
virtual void f() { std::cout << "coucou d'C" << std::endl ; }
} ;

int
main()
{
C unC ;
return 0 ;
}

Ce code doit bien afficher « coucou d'B », parce que lors de
l'appel pa->f(), on est bien dans le constructeur de B. Et ce,
bien que le type statique lors de l'appel est A. C'est que le
polymorphisme est bien actif.

Ce qui veut
dire que la résolution dynamique trouve la version de la
fonction dans Thread. Et la norme dit que si la résolution
dynamique trouve une fonction virtuelle pûre, c'est un
comportement indéfini (même si la fonction a une définition).

Notons que le meme mecanisme
fonctionne a merveille sous Visual Studio 2005.


C'est probablement le résultat d'une optimisation. Le
compilateur, sachant que la fonction est pûre virtuelle, et ne
peut pas être appelée par la résolution dynamique dans le
constructeur, se passe d'initialiser le vtable pour la classe de
base, mais l'initialise immédiatement pour la classe dérivée.


La, je ne te suis pas. Quand tu parles "d'initialiser la vtable pour la
classe de base", tu parles de la table elle-meme ou du pointeur __vtbl
dans l'objet?


Le pointeur, évidemment.

Si on regarde une implémentation classique, on aurait quelque
chose du genre (en pseudo-C, mais avec les noms des fonctions du
C++) :

Base::Base()
{
__vptr = &Base::__vtable ;
// ...
} ;

Derived::Derived()
{
Base::Base() ;
__vptr = &Derived::__vtable ;
// ...
} ;

Mais si toutes les fonctions virtuelles de Base sont pûre, il
pourrait bien le transformer en :

Base::Base()
{
// ...
} ;

Derived::Derived()
{
__vptr = &Derived::__vtable ;
Base::Base() ;
// ...
} ;

parce que si toutes les fonctions virtuelles de Base sont pûre,
le compilateur sait qu'on ne les appelerait jamais à travers le
vtable.

C'est une petite optimisation, une affectation de moins. Et dans
la mesure où des objets polymorphiques sont prèsque toujours
alloués dynamiquement, je me démande si ça vaut la peine. Mais
c'est toujours ça de gagner (et prèsque toujours, ce n'est pas
toujours -- il y a bien des variables locales polymorphiques
parfois, dans le modèle de visiteur, par exemple, et si l'objet
est très simple, ça peut faire une différence).

Pour moi, la seule explication de ce comportement, c'est
l'absence d'ajustement de __vtbl dans le constructeur qui fait que
l'objet est du type Derivee au lieu d'etre du type Base et donc il
continue de voir les methodes de Derivee. C'est donc un bug du compilateu r.


Ce n'est qu'un bug du compilateur que si on appelle une fonction
non-pûre implémentée dans Base, et qu'on exécute le code dans
Derived. Autant que Base ne contient pas de fonction virtuelle
non-pûre, c'est une optimisation tout à fait valable.

--
James Kanze (GABI Software) email:
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
James Kanze
On Apr 4, 11:08 am, "Dominique Vaufreydaz" wrote:

Note que dans l'exemple qu'il a posté en réponse à Fabien, il a
exactement ce problème : sa fonction Run() sera appelée avant
que la variable id sera initialisé, et même l'incrémentation
pourrait avoir lieu avant l'initialisation. (Fort peu probable à
cause du « sleep(10) », mais toujours possible. Et j'imagine
que dans l'application réele, il n'y a pas toujours un sleep(10)
au début de Run().)


Notons comme je l'ai ecrit que c'etait un test bidon montrant
le probleme. Normalement, je n'ecris pas ce genre de chose.
Je voulais aussi tester dans ce cas, si le comportement du compilo
changeait avec une variable membre accèder dans la fonction Run.


Je m'en doute. Je connais peu de cas dans la pratique où on
démarrerait un thread pour qu'il fasse un sleep de 10
secondes:-).

Vala, vala. Merci donc, j'ai changé l'API de ma classe Thread.
Demarre plus toute seul ;oP Au moins, j'aurais appris qqchose
aujourd'hui.


Si tu la modifies pour qu'elle utilise le modèle stratégie, tu
peux très bien utiliser le demarrage automatique dans le
constructeur. Parce que l'utilisateur serait amené
systèmatiquement à construire le délégué, c-à-d l'objet qui
s'exécute, avant de construire le Thread.

--
James Kanze (GABI Software) email:
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
James Kanze
On Apr 5, 11:32 am, Guillaume GOURDIN wrote:
Ce n'est pas forcément une bonne idée. C'est le vieux argument
du modèle stratégie contre le modèle template


Pourrais-je te demander ce que sont ces 2 modèles stp? Merci!


http://www.lmet.fr/fiche.cgi?_ISBN—82711786442&_WORDSÞsign%20patter ns,
voire en vo
http://www.lmet.fr/fiche.cgi?_ISBN—80201633610&_WORDSÞsign%20patter ns.
(Est-ce que quelqu'un a lu la version française, pour nous dire
si la traduction est acceptable ?)

--
James Kanze (GABI Software) email:
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
James Kanze
On Apr 5, 12:01 pm, Jean-Marc Bourguet wrote:
Guillaume GOURDIN writes:
Ce n'est pas forcément une bonne idée. C'est le vieux argument
du modèle stratégie contre le modèle template


Pourrais-je te demander ce que sont ces 2 modèles stp? Merci!


"Il n'y est pas de probleme qui ne puisse etre resolu par une indirection
de plus."


C'est ce qu'on disait. On disait aussi que dans la programmation
OO, cette règle était remplacer par une qui disait « Il n'y a
pas de problème qui ne puisse être résolu par un niveau
d'héritage de plus. » (Alors, le modèle template, c'est la
solution OO, et le modèle stratégie la solution classique :-).)

En gros, le modele template permet d'adapter le fonctionnement d'une clas se
en substituant des membres virtuels. Le modele strategie permet d'adapter
le fonctionnement d'une classe en lui donnant en parametre une autre clas se
contenant (uniquement?) les membres qui sont adaptables. Il est plus
complique, mais plus souple.


Le modèle template exige que l'on dérive de la classe
principale, et non d'une simple interface. Ça peut augmenter le
couplage. Dans le modèle template, la customisation n'est pas
disponible dans le constructeur ni dans le destructeur ; c'est
le problème qu'on vient de voir. Et comme tu dis, le modèle
stratégie est plus souple ; il permet de changer le
customisation en cour de route, sans détruire l'objet auquel il
s'applique.

Avec le modèle template, en revanche, la customisation a accès à
la classe principale (vue qu'elle en dérive). Ça peut être utile
dans certains cas.

J'ai une tendance à privilégier le modèle stratégie, mais il
faut en connaître les deux, et en choisir selon les
circonstances.

--
James Kanze (GABI Software) email:
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
Dominique Vaufreydaz
Bonjour,

Si tu la modifies pour qu'elle utilise le modèle stratégie, tu
peux très bien utiliser le demarrage automatique dans le
constructeur. Parce que l'utilisateur serait amené
systèmatiquement à construire le délégué, c-à-d l'objet qui
s'exécute, avant de construire le Thread.


Je suis d'accord mais l'API a été fixé et du coup voila quoi.
Je comprends bien l'usage d'une factory qui lancerait un
objet créé, d'ailleurs c'est ce genre de chose que l'on expose
sur l'API UserFriendly (qui n'expose pas les Threads). Bref,
trop tard dans le mondele de conception, en tout cas
pas pour cette version la.

Merci. Doms.

Avatar
Laurent Deniau
James Kanze wrote:
On Apr 5, 12:31 pm, Laurent Deniau wrote:
James Kanze wrote:
On Apr 4, 7:46 am, "Dominique Vaufreydaz" wrote:
Je me suis dit que de toute facon, je ne peux
pas instancier la classe Thread (car abstraite du fait du
virtual ... = 0).

Le probleme qui se pose (sous g++ uniquement avec comme
flags de compilation
-Werror -Wall -pedantic -std=c++98 -fPIC) c'est que je me retrouve avec
des erreurs (et pas a chacune des executions) du genre.
pure virtual method called
terminate called without an active exception
Abort


A priori, pas de debordement ailleurs (je verifie encore)
qui justifierait l'ecrasement de la table des fonctions
virtuelles. Des idées ?


C'est un comportement indéfini. Lors de la construction de
Thread, le type dynamique de l'objet est Thread.


De maniere generale, les objets polymorphiques deviennent monomorphiques
dans leurs constructeurs et destructeurs.


Pas vraiment. Qu'on ait plusieurs niveaux d'héritage, et que les
constructeurs appellent des fonctions externes avec this comme
paramètre, et la polymorphisme devient évident.


Yep, j'ai utilise abusivement monomorphique pour signifier qu'il a le
meme type que celui du constructeur. Mea culpa, j'etais surement deja en
vacances (ou presse de l'etre).

[...]

C'est probablement le résultat d'une optimisation. Le
compilateur, sachant que la fonction est pûre virtuelle, et ne
peut pas être appelée par la résolution dynamique dans le
constructeur, se passe d'initialiser le vtable pour la classe de
base, mais l'initialise immédiatement pour la classe dérivée.


La, je ne te suis pas. Quand tu parles "d'initialiser la vtable pour la
classe de base", tu parles de la table elle-meme ou du pointeur __vtbl
dans l'objet?


Le pointeur, évidemment.


Ok.

Si on regarde une implémentation classique, on aurait quelque
chose du genre (en pseudo-C, mais avec les noms des fonctions du
C++) :

Base::Base()
{
__vptr = &Base::__vtable ;
// ...
} ;

Derived::Derived()
{
Base::Base() ;
__vptr = &Derived::__vtable ;
// ...
} ;

Mais si toutes les fonctions virtuelles de Base sont pûre, il
pourrait bien le transformer en :

Base::Base()
{
// ...
} ;

Derived::Derived()
{
__vptr = &Derived::__vtable ;
Base::Base() ;
// ...
} ;

parce que si toutes les fonctions virtuelles de Base sont pûre,
le compilateur sait qu'on ne les appelerait jamais à travers le
vtable.


L'inversion dans le dernier cas est futile a mon sens et je crois que
les choses sont assez compliquee pour que le compilateur reste sur une
methodologie simple et systematique.

Derived::Derived()
{
Base::Base() ;
__vptr = &Derived::__vtable ;
// ...
} ;

Fonctionne dans tous les cas et l'optimisation reste possible dans
Base::Base(). L'interet de cette optimisation, c'est qu'elle depend
uniquement du contexte local. Pour un cas un peu complique, tu peux
regarder le constructeur de la classe DCABBA dans:

http://cern.ch/laurent.deniau/html/cpp_object_model.tgz
fichier: object_model/DCABBA_layout.c

C'est du C qui compile et qui reproduit le modele objet de g++:

void DCABBA_ctor(register DCABBA* const this,
long a, long b, long c, long d,
long ab, long ba, long abba, long dcabba)
{
B_ctor(BASE(this,B), b); /* compiler + user */
A_ctor(BASE(this,A), a); /* compiler + user */
D_ctor((D*)&this->d, d); /* compiler + user */
C_ctor_((C*)this, DCABBA_vtt, b, c); /* compiler + user */
ABBA_ctor_(BASE(this,ABBA), DCABBA_vtt+2, a, b, ab, ba, abba);
/* compiler + user */
this->vptr = DCABBA_vtbl; /* compiler */
this->ABBA_vptr = DCABBA_ABBA_vtbl; /* compiler */
this->BA_vptr = DCABBA_BA_vtbl; /* compiler */
this->B_vptr = DCABBA_B_vtbl; /* compiler */
this->A_vptr = DCABBA_A_vtbl; /* compiler */
this->dcabba = dcabba; /* user */

print_layout("DCABBA", "ctor", this, sizeof *this);
}

Tu trouveras le code C++ beaucoup plus lisible dans object_model.cpp.

C'est une petite optimisation, une affectation de moins. Et dans
la mesure où des objets polymorphiques sont prèsque toujours
alloués dynamiquement, je me démande si ça vaut la peine. Mais
c'est toujours ça de gagner (et prèsque toujours, ce n'est pas
toujours -- il y a bien des variables locales polymorphiques
parfois, dans le modèle de visiteur, par exemple, et si l'objet
est très simple, ça peut faire une différence).

Pour moi, la seule explication de ce comportement, c'est
l'absence d'ajustement de __vtbl dans le constructeur qui fait que
l'objet est du type Derivee au lieu d'etre du type Base et donc il
continue de voir les methodes de Derivee. C'est donc un bug du compilateur.


Ce n'est qu'un bug du compilateur que si on appelle une fonction
non-pûre implémentée dans Base, et qu'on exécute le code dans
Derived. Autant que Base ne contient pas de fonction virtuelle
non-pûre, c'est une optimisation tout à fait valable.


Mais dans le cas present, le comportement est comme si le __vptr n'avait
pas ete change et donc cela ressemble a un bug. Certe, on peut
argumenter que c'est un comportement indefini comme un autre, mais je
prefere de loin le comportement de g++ qui ne laisse aucune ambiguite
sur le fait que le code est incorrecte. Pour confirmer que c'est un bug,
il faudrait trouver un code correct qui n'a pas le comportement attendu.

a+, ld.





Avatar
James Kanze
On Apr 10, 3:56 pm, Laurent Deniau wrote:
James Kanze wrote:
On Apr 5, 12:31 pm, Laurent Deniau wrote:

Si on regarde une implémentation classique, on aurait quelque
chose du genre (en pseudo-C, mais avec les noms des fonctions du
C++) :

Base::Base()
{
__vptr = &Base::__vtable ;
// ...
} ;

Derived::Derived()
{
Base::Base() ;
__vptr = &Derived::__vtable ;
// ...
} ;

Mais si toutes les fonctions virtuelles de Base sont pûre, il
pourrait bien le transformer en :

Base::Base()
{
// ...
} ;

Derived::Derived()
{
__vptr = &Derived::__vtable ;
Base::Base() ;
// ...
} ;

parce que si toutes les fonctions virtuelles de Base sont pûre,
le compilateur sait qu'on ne les appelerait jamais à travers le
vtable.


L'inversion dans le dernier cas est futile a mon sens et je crois que
les choses sont assez compliquee pour que le compilateur reste sur une
methodologie simple et systematique.

Derived::Derived()
{
Base::Base() ;
__vptr = &Derived::__vtable ;
// ...
} ;

Fonctionne dans tous les cas et l'optimisation reste possible dans
Base::Base(). L'interet de cette optimisation, c'est qu'elle depend
uniquement du contexte local.


À vrai dire, c'est ce que j'aurais cru moi-même. En ayant
régardé le code généré, en revanche, je me rappelle bien d'avoir
vu un cas où l'initialisation s'est fait avant l'appel du
constructeur de Base.

En y refléchissant, je me démande s'il n'y avait d'héritage
virtuel. Il me sembe que je me serais souvenu si c'était le cas,
mais dans le cas d'héritage virtuel, on pourrait comprendre. Le
_vtable alors comprend des informations pour rétrouver la base
virtuelle, dont la position dépend de la classe finale la plus
dérivée. C'est donc à l'appelant d'initialiser le _vptr, et non
à l'appelé. (C'est la distinction, en g++, par exemple, entre
les deux points d'entrée des constructeurs, le « in charge »,
et l'autre. Un est appelé quand il s'agit de la classe la plus
dérivée ; il s'occupe de l'initialisation des classes de base
virtuelles et des _vptr. L'autre est appelé depuis les
constructeurs dérivés, et omet ces initialisations.)

Pour un cas un peu complique, tu peux
regarder le constructeur de la classe DCABBA dans:

http://cern.ch/laurent.deniau/html/cpp_object_model.tgz
fichier: object_model/DCABBA_layout.c

C'est du C qui compile et qui reproduit le modele objet de g++:

void DCABBA_ctor(register DCABBA* const this,
long a, long b, long c, long d,
long ab, long ba, long abba, long dcabba)
{
B_ctor(BASE(this,B), b); /* compiler + user */
A_ctor(BASE(this,A), a); /* compiler + user */
D_ctor((D*)&this->d, d); /* compiler + user */
C_ctor_((C*)this, DCABBA_vtt, b, c); /* compiler + user */
ABBA_ctor_(BASE(this,ABBA), DCABBA_vtt+2, a, b, ab, ba, abba);
/* compiler + user */
this->vptr = DCABBA_vtbl; /* compiler */
this->ABBA_vptr = DCABBA_ABBA_vtbl; /* compiler */
this->BA_vptr = DCABBA_BA_vtbl; /* compiler */
this->B_vptr = DCABBA_B_vtbl; /* compiler */
this->A_vptr = DCABBA_A_vtbl; /* compiler */
this->dcabba = dcabba; /* user */

print_layout("DCABBA", "ctor", this, sizeof *this);

}

Tu trouveras le code C++ beaucoup plus lisible dans object_model.cpp.


C'est intéressant. (On voit que tu y a mis du travail, à le
rendre compréhensible.) Donc, en fait, le vptr des classes avec
des bases virtuelles est passé en paramètre aux constructeur de
la classe de base (c'est le deuxième paramètre de C_ctor_ et de
ABBA_ctor_, si je ne me trompe pas.)

C'est possible, évidemment, que ça a changé selon la version, et
que mes souvenirs se rapportent à une version plus ancienne. Ou
à un autre compilateur, ou carrément que je me souviens mal --
ça fait longtemps que je n'ai pas eu à jetter un coup d'oeil au
code généré.

C'est une petite optimisation, une affectation de moins. Et dans
la mesure où des objets polymorphiques sont prèsque toujours
alloués dynamiquement, je me démande si ça vaut la peine. Mais
c'est toujours ça de gagner (et prèsque toujours, ce n'est pas
toujours -- il y a bien des variables locales polymorphiques
parfois, dans le modèle de visiteur, par exemple, et si l'objet
est très simple, ça peut faire une différence).

Pour moi, la seule explication de ce comportement, c'est
l'absence d'ajustement de __vtbl dans le constructeur qui fait que
l'objet est du type Derivee au lieu d'etre du type Base et donc il
continue de voir les methodes de Derivee. C'est donc un bug du compila teur.


Ce n'est qu'un bug du compilateur que si on appelle une fonction
non-pûre implémentée dans Base, et qu'on exécute le code dans
Derived. Autant que Base ne contient pas de fonction virtuelle
non-pûre, c'est une optimisation tout à fait valable.


Mais dans le cas present, le comportement est comme si le __vptr n'avait
pas ete change et donc cela ressemble a un bug. Certe, on peut
argumenter que c'est un comportement indefini comme un autre, mais je
prefere de loin le comportement de g++ qui ne laisse aucune ambiguite
sur le fait que le code est incorrecte.


Tout à fait. Je n'ai rien dit du point de vue de la qualité de
l'implémentation -- là, je suis tout à fait d'accord qu'il faut
avorter le programme le plus vite possible (avec un message
d'erreur). En fait, je me suis souvent démandé pourquoi aucune
implémentation n'a jamais défini des handlers pour de telles
choses, du même genre que les new_handler ou unexpected_handler.
Ça permettrait à mon code de prendre la main un petit instant,
pour sortir un message où moi, je le veux (puisque dans mes
applications, std::cerr va normallement à "/dev/null"), et
formatté comme je veux. (La même chose vaut pour assert,
d'ailleurs. Mais assert, je peux le changer avec ma version,
s'il le faut.)

Pour confirmer que c'est un bug, il faudrait trouver un code
correct qui n'a pas le comportement attendu.


On peut aussi arguer sur le plan « qualité
d'implémentation » ; tous les implémenteurs le prenent en
considération. Mais évidemment, c'est plus difficile ; ça ne
marche que quand on est d'accord sur ce qui est le comportement
souhaitable. (Certains semblent parfois vouloir cacher les
erreurs, et préfèrent que le programme donne un résultat erroné
et trompeur à ce qu'il crashe. Ou qu'il crashe plus tard. Ce qui
vaut pour les programmes de démo, dans les foires, où
l'observateur s'en fiche des résultats et n'a pas le temps de
les vérifier, et où plus tard signifie -- on l'espère, au
moins -- après que le client potentiel a déjà tourné le dos. En
dehors des foires, en revanche, je le trouve un peu
irresponsable.)

--
James Kanze (GABI Software) email:
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
Laurent Deniau
James Kanze wrote:
On Apr 10, 3:56 pm, Laurent Deniau wrote:
James Kanze wrote:
On Apr 5, 12:31 pm, Laurent Deniau wrote:




[...]

parce que si toutes les fonctions virtuelles de Base sont pûre,
le compilateur sait qu'on ne les appelerait jamais à travers le
vtable.


L'inversion dans le dernier cas est futile a mon sens et je crois que
les choses sont assez compliquee pour que le compilateur reste sur une
methodologie simple et systematique.

Derived::Derived()
{
Base::Base() ;
__vptr = &Derived::__vtable ;
// ...
} ;

Fonctionne dans tous les cas et l'optimisation reste possible dans
Base::Base(). L'interet de cette optimisation, c'est qu'elle depend
uniquement du contexte local.


À vrai dire, c'est ce que j'aurais cru moi-même. En ayant
régardé le code généré, en revanche, je me rappelle bien d'avoir
vu un cas où l'initialisation s'est fait avant l'appel du
constructeur de Base.


Je soupconnerais un cas d'inlining avec reorganisation des instructions
par l'optimiseur.

En y refléchissant, je me démande s'il n'y avait d'héritage
virtuel. Il me sembe que je me serais souvenu si c'était le cas,
mais dans le cas d'héritage virtuel, on pourrait comprendre. Le
_vtable alors comprend des informations pour rétrouver la base
virtuelle, dont la position dépend de la classe finale la plus
dérivée. C'est donc à l'appelant d'initialiser le _vptr, et non
à l'appelé. (C'est la distinction, en g++, par exemple, entre
les deux points d'entrée des constructeurs, le « in charge »,
et l'autre. Un est appelé quand il s'agit de la classe la plus
dérivée ; il s'occupe de l'initialisation des classes de base
virtuelles et des _vptr. L'autre est appelé depuis les
constructeurs dérivés, et omet ces initialisations.)


A moins que le modele ait change depuis 2004 (date a laquelle j'ai ecrit
le debut de cet article), la distinction viendrait du passage de la
vtable en argument. Chaque constructeur a donc deux versions (cf ci-apres).

Pour un cas un peu complique, tu peux
regarder le constructeur de la classe DCABBA dans:

http://cern.ch/laurent.deniau/html/cpp_object_model.tgz
fichier: object_model/DCABBA_layout.c

C'est du C qui compile et qui reproduit le modele objet de g++:

void DCABBA_ctor(register DCABBA* const this,
long a, long b, long c, long d,
long ab, long ba, long abba, long dcabba)
{
B_ctor(BASE(this,B), b); /* compiler + user */
A_ctor(BASE(this,A), a); /* compiler + user */
D_ctor((D*)&this->d, d); /* compiler + user */
C_ctor_((C*)this, DCABBA_vtt, b, c); /* compiler + user */
ABBA_ctor_(BASE(this,ABBA), DCABBA_vtt+2, a, b, ab, ba, abba);
/* compiler + user */
this->vptr = DCABBA_vtbl; /* compiler */
this->ABBA_vptr = DCABBA_ABBA_vtbl; /* compiler */
this->BA_vptr = DCABBA_BA_vtbl; /* compiler */
this->B_vptr = DCABBA_B_vtbl; /* compiler */
this->A_vptr = DCABBA_A_vtbl; /* compiler */
this->dcabba = dcabba; /* user */

print_layout("DCABBA", "ctor", this, sizeof *this);

}

Tu trouveras le code C++ beaucoup plus lisible dans object_model.cpp.


C'est intéressant. (On voit que tu y a mis du travail, à le
rendre compréhensible.)


La partie code m'avait pris quelques jours en revanche la partie html
(le papier) m'avait pris plus d'une semaine (j'ai beaucoup joue avec les
css). C'est ce qui fait que le code est complet et pas le papier ;-)

Donc, en fait, le vptr des classes avec
des bases virtuelles est passé en paramètre aux constructeur de
la classe de base (c'est le deuxième paramètre de C_ctor_ et de
ABBA_ctor_, si je ne me trompe pas.)


C'est bien ca, vtt = virtual table table (parce qu'il peut y en avoir
plusieurs). Ca permet de garder un contexte local pour C_ctor_ qui
decidera des affectations necessaires. X_ctor = constructeur, X_ctor_ =
constructeur avec vtt. Pour ABBA il y aussi un ajustement d'offset dans
la vtt. Tu peux aussi regarder DCABBA_ctor_ un peu plus loin dans le
meme fichier (section ctor partiels) pour voir la difference et les
implications.

C'est possible, évidemment, que ça a changé selon la version, et
que mes souvenirs se rapportent à une version plus ancienne. Ou
à un autre compilateur, ou carrément que je me souviens mal --
ça fait longtemps que je n'ai pas eu à jetter un coup d'oeil au
code généré.


Ce modele est celui decrit dans le papier de Nathan Sidwell de 2003 (la
ref est dans le html), ou du moins ce que j'en ai compris. J'ai essaye
de mettre en place ce que propose Natalie Eckel mais c'est vraiment trop
complique et n'apporte rien a l'aspect didactique du papier.

En fait ce papier est le resultat d'une discussion avec Gaby ici-meme ou
il m'avait appris que le seul moyen de respecter la norme etait de
reaffecter les vptr de this. Comme j'avais lu que les hierarchies
complexes de classes polymorphiques pouvaient faire gonfler le code,
j'ai voulu voir les implications de ces reaffectations. Et j'ai trouve
que meme dans le cas de DCABBA qui est artificiellement compliquee
(inclue tous les cas possibles), les vtables annexes ne sont pas si
terrible que ca. Certe cette classe a 17 vtables et 1 vtt mais ca n'est
pas si gros que ca au final. Ou disont en tout cas que le modele objet
de g++ est un bon compromis d'optimisation entre espace et rapidite.

Par contre, une chose que je n'ai pas comprise, c'est pourquoi mon
implementation naive de dynamic_cast est trois fois plus rapide que
celle de g++, alors qu'il a acces a bien plus d'information (hierarchie
simple ou en diamant).

On peut aussi arguer sur le plan « qualité
d'implémentation » ; tous les implémenteurs le prenent en
considération. Mais évidemment, c'est plus difficile ; ça ne
marche que quand on est d'accord sur ce qui est le comportement
souhaitable. (Certains semblent parfois vouloir cacher les
erreurs, et préfèrent que le programme donne un résultat erroné
et trompeur à ce qu'il crashe. Ou qu'il crashe plus tard. Ce qui
vaut pour les programmes de démo, dans les foires, où
l'observateur s'en fiche des résultats et n'a pas le temps de
les vérifier, et où plus tard signifie -- on l'espère, au
moins -- après que le client potentiel a déjà tourné le dos. En
dehors des foires, en revanche, je le trouve un peu
irresponsable.)


;-)

a+, ld.



Avatar
James Kanze
On Apr 11, 10:56 am, Laurent Deniau wrote:
James Kanze wrote:
On Apr 10, 3:56 pm, Laurent Deniau wrote:
James Kanze wrote:
On Apr 5, 12:31 pm, Laurent Deniau wrote:




[...]
parce que si toutes les fonctions virtuelles de Base sont pûre,
le compilateur sait qu'on ne les appelerait jamais à travers le
vtable.


L'inversion dans le dernier cas est futile a mon sens et je crois que
les choses sont assez compliquee pour que le compilateur reste sur une
methodologie simple et systematique.

Derived::Derived()
{
Base::Base() ;
__vptr = &Derived::__vtable ;
// ...
} ;

Fonctionne dans tous les cas et l'optimisation reste possible dans
Base::Base(). L'interet de cette optimisation, c'est qu'elle depend
uniquement du contexte local.


À vrai dire, c'est ce que j'aurais cru moi-même. En ayant
régardé le code généré, en revanche, je me rappelle bien d'av oir
vu un cas où l'initialisation s'est fait avant l'appel du
constructeur de Base.


Je soupconnerais un cas d'inlining avec reorganisation des instructions
par l'optimiseur.


C'est aussi une possibilité. Il faut dire que ça fait un moment
que j'y ai régardé, et que je ne me rappelle plus ni le
compilateur, ni même le système.

En y refléchissant, je me démande s'il n'y avait d'héritage
virtuel. Il me sembe que je me serais souvenu si c'était le cas,
mais dans le cas d'héritage virtuel, on pourrait comprendre. Le
_vtable alors comprend des informations pour rétrouver la base
virtuelle, dont la position dépend de la classe finale la plus
dérivée. C'est donc à l'appelant d'initialiser le _vptr, et non
à l'appelé. (C'est la distinction, en g++, par exemple, entre
les deux points d'entrée des constructeurs, le « in charge »,
et l'autre. Un est appelé quand il s'agit de la classe la plus
dérivée ; il s'occupe de l'initialisation des classes de base
virtuelles et des _vptr. L'autre est appelé depuis les
constructeurs dérivés, et omet ces initialisations.)


A moins que le modele ait change depuis 2004 (date a laquelle j'ai ecrit
le debut de cet article), la distinction viendrait du passage de la
vtable en argument. Chaque constructeur a donc deux versions (cf ci-apres ).


Depuis 2004, je ne crois pas. Entre le moment que j'ai régardé
le généré et 2004, c'est fort possible.

[...]
Donc, en fait, le vptr des classes avec
des bases virtuelles est passé en paramètre aux constructeur de
la classe de base (c'est le deuxième paramètre de C_ctor_ et de
ABBA_ctor_, si je ne me trompe pas.)


C'est bien ca, vtt = virtual table table (parce qu'il peut y en avoir
plusieurs). Ca permet de garder un contexte local pour C_ctor_ qui
decidera des affectations necessaires. X_ctor = constructeur, X_ctor_ =
constructeur avec vtt. Pour ABBA il y aussi un ajustement d'offset dans
la vtt. Tu peux aussi regarder DCABBA_ctor_ un peu plus loin dans le
meme fichier (section ctor partiels) pour voir la difference et les
implications.

C'est possible, évidemment, que ça a changé selon la version, et
que mes souvenirs se rapportent à une version plus ancienne. Ou
à un autre compilateur, ou carrément que je me souviens mal --
ça fait longtemps que je n'ai pas eu à jetter un coup d'oeil au
code généré.


Ce modele est celui decrit dans le papier de Nathan Sidwell de 2003 (la
ref est dans le html), ou du moins ce que j'en ai compris. J'ai essaye
de mettre en place ce que propose Natalie Eckel mais c'est vraiment trop
complique et n'apporte rien a l'aspect didactique du papier.

En fait ce papier est le resultat d'une discussion avec Gaby ici-meme ou
il m'avait appris que le seul moyen de respecter la norme etait de
reaffecter les vptr de this.


C'est à peu près ce que la norme exige. Au « as if » près ;
si, par exemple, toutes les fonctions virtuelles de la classe de
base sont pûres, le compilateur peut s'en passer, parce qu'il
sait que le vtable ne servira jamais dans la classe de base.

Comme j'avais lu que les hierarchies
complexes de classes polymorphiques pouvaient faire gonfler le code,
j'ai voulu voir les implications de ces reaffectations. Et j'ai trouve
que meme dans le cas de DCABBA qui est artificiellement compliquee
(inclue tous les cas possibles), les vtables annexes ne sont pas si
terrible que ca. Certe cette classe a 17 vtables et 1 vtt mais ca n'est
pas si gros que ca au final. Ou disont en tout cas que le modele objet
de g++ est un bon compromis d'optimisation entre espace et rapidite.


Je crois que c'est le cas de tous les compilateurs aujourd'hui.
La réputation de gonflage vient peut-être de CFront, je me
rappelle un essai une fois avec un hièrarchie un peu complexe où
la taille de l'objet (non du code) était quarante fois plus
grande avec CFront qu'avec g++ ou aCC (de HP). Il faut dire
qu'il y avait un bug dans CFront dans la gestion de l'héritage
virtuel, qui faisait qu'il réservait la place pour la classe de
base virtuelle autant de fois qu'on le citait dans les
déclarations, même si par la suite, il n'y avait qu'une instance
qui servait.

Par contre, une chose que je n'ai pas comprise, c'est pourquoi mon
implementation naive de dynamic_cast est trois fois plus rapide que
celle de g++, alors qu'il a acces a bien plus d'information (hierarchie
simple ou en diamant).


Je ne sais pas, mais je sais qu'il y a plusieurs choses dans
dynamic_cast qui peut le rallentir : il faut qu'il gère les
accès, par exemple, pour ne pas permettre la conversion à une
base privée. Et il peut y avoir du code spécial pour le cas des
objets chargés dynamiquement : je sais que chez Microsoft, il y
une instance des informations par DLL, par exemple, et que donc,
si la comparaison d'adresse échoue, il faut passer par les
comparaisons de chaîne de caractères.

--
James Kanze (GABI Software) email:
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 3