OVH Cloud OVH Cloud

Forme canonique de Coplien et poymorphisme

137 réponses
Avatar
Y a n n R e n a r d
Bonjour à tous,

tout d'abord, merci à tous pour ce newsgroup très instructif, c'est la
première fois que je poste, alors je vais essayer de ne pas me faire
incendier par Fabien ;) :p

Voila mon problème : j'ai un client qui souhaite que toutes les classes
définies dans le projet sur lequel je travaille soient sous la forme
canonique de Coplien (pour ceux qui ne connaissent pas : constructeur
par défaut, constructeur par copie, operateur d'affectation et
destructeur virtuel ou non). Pour que ce soit plus clean (selon eux), le
client demande que toutes les classes dérivent d'une classe de base...
Sauf que je ne vois pas du tout l'intéret de faire ca car je ne vois pas
en quoi ca force à respecter la forme canonique de Coplien.

Concernant l'operateur d'affectation, si je le met virtuel (est ce
quelque chose de propre ?!) comment puis je faire en sorte que les
classes filles utilisent un opérateur correct ? cast dynamique ?

Concernant l'operateur de recopie, là, je vois pas du tout du tout du
tout :)

Bref, je comprends pas du tout cette demande, si quelqu'un pense que
c'est raisonable ou explicable, je veux bien une précision sur l'utilité...

Pour terminer, il faut aussi déporter le code du constructeur et du
destructeur dans une fonction séparée, et là, je comprends plus du
tout... genre quelque chose comme ca :

T::T(void)
{
construct();
}

T::~T(void)
{
destruct();
}

void T::construct(void)
{
// ...
}

void T::destruct(void)
{
// ...
}

Voila voila, votre avis est le bienvenu :)
Merci d'avance,

Yann Renard

10 réponses

Avatar
Laurent Deniau
Jean-Marc Bourguet wrote:
Laurent Deniau writes:


c'est dans F::F() que tu crees l'alias en appellant B::B() avec deux
this avec deux types statiques differents mais le meme type
effectif. Donc p->f() invoque un B::f() qui n'exite pas. Bien vu,
ceci dit tu n'as pas besoin de l'heritage multiple pour avoir le UB:

struct A {
A(A*p) { p->f(); } // invoque A::f(p), Ok mais pas voulu
virtual void f();
};

struct D : A {
D(): A(this) {} // alias
virtual void f();
};

Ce code marchera (contraitement a ton exemple qui met bien les point
sur les 'i' ;-), mais je pense que la norme lui attribue quand meme
un UB, p etant un alias sur D* mais de type effectif A* au meme
titre que this...



Je crois que c'est bien defini. C'est la partie que j'ai coupe de
l'exemple de la norme : le p dans A::A designe le sous-objet en cours
de construction


Juste, mon exemple etait trop simpliste, mais que penses-tu de:

struct D;

struct A {
A(D* p);
virtual void f();
};

struct D : A {
D(): A(this) {} // alias
virtual void f();
};

A::A(D* p) { p->f(); } // appelle A::f() au lieu de D::f()!

cette fois-ci l'alias est explicite mais le type effectif (et le
comportement) est le meme. UB or not UB?

a+, ld.


Avatar
Jean-Marc Bourguet
Laurent Deniau writes:

Jean-Marc Bourguet wrote:
Laurent Deniau writes:

c'est dans F::F() que tu crees l'alias en appellant B::B() avec deux
this avec deux types statiques differents mais le meme type
effectif. Donc p->f() invoque un B::f() qui n'exite pas. Bien vu,
ceci dit tu n'as pas besoin de l'heritage multiple pour avoir le UB:

struct A {
A(A*p) { p->f(); } // invoque A::f(p), Ok mais pas voulu
virtual void f();
};

struct D : A {
D(): A(this) {} // alias
virtual void f();
};

Ce code marchera (contraitement a ton exemple qui met bien les point
sur les 'i' ;-), mais je pense que la norme lui attribue quand meme
un UB, p etant un alias sur D* mais de type effectif A* au meme
titre que this...
Je crois que c'est bien defini. C'est la partie que j'ai coupe de

l'exemple de la norme : le p dans A::A designe le sous-objet en cours
de construction


Juste, mon exemple etait trop simpliste, mais que penses-tu de:

struct D;

struct A {
A(D* p);
virtual void f();
};

struct D : A {
D(): A(this) {} // alias
virtual void f();
};

A::A(D* p) { p->f(); } // appelle A::f() au lieu de D::f()!

cette fois-ci l'alias est explicite mais le type effectif (et le
comportement) est le meme. UB or not UB?


Pour moi, p designe autre chose que le sous-objet en cours de
contruction, c'est donc un UB.

A+

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



Avatar
Laurent Deniau
Jean-Marc Bourguet wrote:
Laurent Deniau writes:


Jean-Marc Bourguet wrote:

Laurent Deniau writes:


c'est dans F::F() que tu crees l'alias en appellant B::B() avec
deux this avec deux types statiques differents mais le meme
type effectif. Donc p->f() invoque un B::f() qui n'exite pas.
Bien vu, ceci dit tu n'as pas besoin de l'heritage multiple
pour avoir le UB:

struct A { A(A*p) { p->f(); } // invoque A::f(p), Ok mais pas
voulu virtual void f(); };

struct D : A { D(): A(this) {} // alias virtual void f(); };

Ce code marchera (contraitement a ton exemple qui met bien les
point sur les 'i' ;-), mais je pense que la norme lui attribue
quand meme un UB, p etant un alias sur D* mais de type effectif
A* au meme titre que this...


Je crois que c'est bien defini. C'est la partie que j'ai coupe
de l'exemple de la norme : le p dans A::A designe le sous-objet
en cours de construction


Juste, mon exemple etait trop simpliste, mais que penses-tu de:

struct D;

struct A { A(D* p); virtual void f(); };

struct D : A { D(): A(this) {} // alias virtual void f(); };

A::A(D* p) { p->f(); } // appelle A::f() au lieu de D::f()!

cette fois-ci l'alias est explicite mais le type effectif (et le
comportement) est le meme. UB or not UB?



Pour moi, p designe autre chose que le sous-objet en cours de
contruction,


Et pourtant non, comme dans mon premier exemple.

c'est donc un UB.


Je suis d'accord ;-) Par rapport mon exemple precedent je n'ai fait que
rendre explicite l'alias. Voila pourquoi je voulais savoir si le premier
exemple avait aussi un UB, parce que concretement c'est la meme chose.
Je trouve que les deux devraient etre des UB, que l'alias soit explicite
ou implicite, notament parce que ni l'un, ni l'autre ne font ce qui est
attendu - invoquer D::f() - si p n'etait pas this lui-meme (certe ce
n'est pas une raison en soit).

a+, ld.




Avatar
Laurent Deniau
Laurent Deniau wrote:
Jean-Marc Bourguet wrote:

Laurent Deniau writes:


Jean-Marc Bourguet wrote:

Laurent Deniau writes:


c'est dans F::F() que tu crees l'alias en appellant B::B() avec
deux this avec deux types statiques differents mais le meme type
effectif. Donc p->f() invoque un B::f() qui n'exite pas. Bien vu,
ceci dit tu n'as pas besoin de l'heritage multiple pour avoir le UB:

struct A { A(A*p) { p->f(); } // invoque A::f(p), Ok mais pas voulu
virtual void f(); };

struct D : A { D(): A(this) {} // alias virtual void f(); };

Ce code marchera (contraitement a ton exemple qui met bien les
point sur les 'i' ;-), mais je pense que la norme lui attribue
quand meme un UB, p etant un alias sur D* mais de type effectif
A* au meme titre que this...



Je crois que c'est bien defini. C'est la partie que j'ai coupe de
l'exemple de la norme : le p dans A::A designe le sous-objet en
cours de construction



Juste, mon exemple etait trop simpliste, mais que penses-tu de:

struct D;

struct A { A(D* p); virtual void f(); };

struct D : A { D(): A(this) {} // alias virtual void f(); };

A::A(D* p) { p->f(); } // appelle A::f() au lieu de D::f()!

cette fois-ci l'alias est explicite mais le type effectif (et le
comportement) est le meme. UB or not UB?




Pour moi, p designe autre chose que le sous-objet en cours de
contruction,



Et pourtant non, comme dans mon premier exemple.

c'est donc un UB.



Je suis d'accord ;-) Par rapport mon exemple precedent je n'ai fait que
rendre explicite l'alias. Voila pourquoi je voulais savoir si le premier
exemple avait aussi un UB, parce que concretement c'est la meme chose.
Je trouve que les deux devraient etre des UB, que l'alias soit explicite
ou implicite, notament parce que ni l'un, ni l'autre ne font ce qui est
attendu - invoquer D::f() - si p n'etait pas this lui-meme (certe ce
n'est pas une raison en soit).


D'ailleurs si les deux sont un UB - ce qui n'est apparement pas le cas
pour l'instant -, cela peut changer fondamentalement la facon de voir
des compilateurs C++ en remplacant la regle:

- this est du type T* dans les ctors et dtor de la classe T
(ce qui oblige a mettre a jour tous les __vtbl de l'objet
pendant sa transition a travers les ctor/dtor de sa hierarchie)

en

- toute resolution de methode basee sur *this* est statique.
this->f() invoque T::f() ou T* est le type de this ou celui d'un
parent. (Est-ce que ca leverait la necessite de modifier les __vtbl?)

Mon exemple ci-dessus planterait puisque p->f() invoquerait D::f() avec
un this en A*, mais comme ca serait un UB ca ne serait plus a l'ordre du
jour.

La deuxieme regle etant plus simple a mettre en oeuvre et moins couteuse
que la premiere, je me doute qu'il y a une autre raison pour qu'elle
n'ait pas ete adoptee. Mais laquelle? (question liee a celle de la regle
en question).

a+, ld.





Avatar
Gabriel Dos Reis
Laurent Deniau writes:

| Laurent Deniau wrote:
| > Jean-Marc Bourguet wrote:
| >
| >> Laurent Deniau writes:
| >>
| >>> Donc B* devient un A* et reste un A*.
| [...]
|
| >> delete a;
| >>
| >> peut donc être compilé en qqch du genre (j'ai jamais
| >> travaillé sur un compilateur C++, il y a peut-être des
| >> choses que j'oublie):
| >>
| >> __base = a->__get_most_derived_object();
| >> __sz = a->__get_size_of_most_derived_object();
| >> __delete = a->__get_delete_operator();
| >> a->__destructor_in_charge_of_virtual_members(__base);
| >> (*__delete)(__base, __sz);
| > Je pense que tu as raison. Je viens de faire un test (code a la fin)
| > qui montre que les deux sont compatibles: transition du type dans
| > les ctor/dtor + delete/RTTI virtuels (j'aurais du commencer par
| > la). Par contre aux vues de ce que Gaby a explique, je ne vois pas
| > comment le compilo peut s'en sortir sans reaffecter le __vtbl de
| > l'objet lorsqu'il mute l'objet entre deux dtors. Si en entrant dans
| > A::~A(), this qui etait un B* devient vraiment un A*, __vtbl doit
| > necessairement changer. Si ce n'est pas ce qu'il fait, j'aimerais
| > bien que qqun m'explique comment le compilo s'en sort avec l'appel
| > de f2() dans A::f1()?
|
| Pour lever cette question, il est facile de rajouter deux lignes dans
| le code que j'ai propose. Effectivement, le compilateur change les
| __vtbl des objets pendant la transition des ctor/dtor (en tout cas gcc
| 3.3.4 le fait).

C'est effectivement ce que fait GCC, c'est ce qui est mandaté par
l'ABI commune et c'est nécessaire pour implémenter correctement et
efficacement le modèle objet C++ :-)

| Je comprends mieux pourquoi cote assembleur le compilo
| utilise des 'frames' plutot qu'une bete copie d'une instance statique
| deja initialise a vide mais avec les bons __vtbl (ce que fait
| oopc). Dans le cas de l'heritage multiple, chaque transition de
| ctor/dtor doit rajouter un coup non negligeable au runtime.

yep, mais il est supposé que le coût de construction/destruction d'un
objet avec héritage multiple est négligeable par rapport aux
« services » qu'on en tire, dans les applications « typiques ».

-- Gaby
Avatar
Gabriel Dos Reis
writes:

| Gabriel Dos Reis wrote in message
| news:...
| > writes:
|
| > | Jean-Marc Bourguet wrote in message
| > | news:...
| > | > Gabriel Dos Reis writes:
|
| > | > > | > #include <iostream>
|
| > | > > | > struct A {
| > | > > | > virtual void f() = 0;
| > | > > | > virtual ~A() { f(); }
| > | > > | > };
|
| > | > > | > void A::f() { std::cout << "coucou !n"; }
|
| > | > > | Oui, je vois, tu as raison aussi :-)
|
| > | > > Non. Je n'ai pas raison : la fonction est *définie*, mais le
| > | > > code invoque un fonctionnement indéfini (la plupart des
| > | > > compilateurs l'accepteront sur la base du méchanisme d'appel).
|
| > | > Evidemment ils ont ete mettre en 10.4/3 et une clause qui
| > | > contredit ce qu'on peut penser en ne regardant que 12.7 (et en
| > | > particulier 12.7/3).
|
| > | > Je comprends que l'effet soint indefini quand la fonction n'est
| > | > pas definie, mais si elle l'est ca me choque un peu.
|
| > | Je ne sais pas. C'est le résultat d'un ensemble de règles, chacune
| > | simple et logique en soi :
| > | - la résolution dynamique ne trouve jamais une fonction virtuelle
| > | pûre,
|
| > Cette règle ne me semble ni simple ni logique en soi.
|
| Alors, avec quoi est-ce que tu proposes de la remplacer, étant donné

Les fonctions virtuelles (pures et impures) sont résolues sur la
base des types dynamiques.


| qu'on n'est pas obligé à fournir une définition pour une fonction
| virtuelle pure. Qu'on soit obligé à fournir une définition ? Ou que la

Si tu l'utilises tu en fournis la définition. Point.
C'est que la norme demande actuellement.

| résolution dynamique trouve peut-être une fonction virtuelle pûre, et
| peut-être pas, selon ce qui se trouve dans d'autres unités de
| compilation, que tu ne connais pas ?

Si tu écris du code « peut-être », tu auras du fonctionnement
« peut-être ».

-- Gaby
Avatar
Gabriel Dos Reis
Laurent Deniau writes:

[...]

| >>| Je ne sais pas. C'est le résultat d'un ensemble de règles, chacune
| >>| simple et logique en soi :
| >>| - la résolution dynamique ne trouve jamais une fonction virtuelle
| >>| pûre,
| >
| >>Cette règle ne me semble ni simple ni logique en soi.
| > Alors, avec quoi est-ce que tu proposes de la remplacer, étant donné
| > qu'on n'est pas obligé à fournir une définition pour une fonction
| > virtuelle pure. Qu'on soit obligé à fournir une définition ? Ou que la
| > résolution dynamique trouve peut-être une fonction virtuelle pûre, et
| > peut-être pas, selon ce qui se trouve dans d'autres unités de
| > compilation, que tu ne connais pas ?
|
| Sur ce point, je suis d'accord avec Gaby. Pourquoi la deuxieme
| solution n'est pas viable puisque dans la pratique c'est ce qui se
| passe? En quoi est ce que la connaissance des autres unites de
| compilation est importante du moment que l'on sait ou est la vtbl et
| que toute definition s'enregistre dans la vtbl, meme si au depart elle
| etait declaree virtuelle pure.

La réalité est qu'elle ne l'est pas ; mais tu le vois probablement
parce que tu as déjà implémenté un système orienté objet :-)

-- Gaby
Avatar
Gabriel Dos Reis
writes:

| Gabriel Dos Reis wrote in message
| news:...
|
| > writes:
|
| > | Que veux-tu d'autre ?
|
| > Quelque chose d'utile.
|
| > | Dans la pratique, quand le compilateur génère le
| > | vtbl, il ne peut pas savoir si A::f() est définie ou non.
|
| > Il se débrouille bien avec « operator new », il peut aussi se
| > débrouiller pour faire quelque d'utile, qui nécessite juste le mÊme
| > effort.
|
| Ce n'est pas le compilateur qui se débrouille avec operator new. C'est
| l'éditeur de liens.

Pouf, c'est une dictintion profonde.
Sérieusement, je parle du compilateur C++ en tant que le machin qui
produit de l'exécutable. Que tu le découpes en nombre d'Avogadro de
parties ne change fondamentalement rien au problème. Les fonctions
inlines s'en foutent, les templates s'en foutent, les type_info s'en
foutent, et moi aussi.

Et de fait, dans la pratique le comportement que je décris est déjà
implémenté (comme un choix possible de fonctionnement indéfini).

-- Gaby
Avatar
Laurent Deniau
Laurent Deniau wrote:
Laurent Deniau wrote:
- toute resolution de methode basee sur *this* est statique.
this->f() invoque T::f() ou T* est le type de this ou celui d'un
parent. (Est-ce que ca leverait la necessite de modifier les __vtbl?)


Cette regle ne tient pas deux secondes: elle deactive la resolution
vituelle, meme dans le cas general. A force de se focaliser sur un point
on en oublie l'ensemble. Desole, j'ai pas trouve le temps de desavouer
cette debilite plus tot...

a+, ld.

Avatar
kanze
Laurent Deniau wrote in message
news:<co453d$pl$...
Laurent Deniau wrote:
Jean-Marc Bourguet wrote:



[...]

Donc quand Gaby dit que un B* devient un A* en entrant dans A::~A(),
il a complement raison.


Un B* devient un A* dans toute fonction member de A. Quel rapport ?
Quand il y a résolution dynamique d'une fonction, la fonction appelée
dépend du type dynamique de l'objet pointé, non du type du pointeur. Ce
qui importe ici, c'est que le type dynamique de l'objet change quand on
entre dans le destructeur. Le type du pointeur n'a pas beaucoup
d'importance.

Ce qui veut dire ensuite que la resolution des
methodes (virtuelle ou non) reste inchange: pas de semantique
particuliere dans les ctor/dtor.


Tout à fait. On résoud l'appel selon le type dynamique de l'objet, et
non selon le type du pointeur.

Et donc comme dit Gaby, la regle de James est compliquee voir illogique
car cela n'a rien a voir avec la semantique de resolution des appels
virtuels dans les ctor/dtor. Seulement avec la transition du type de
this. Du moins c'est ma conclusion pour l'instant. Des reclamations ? ;-)


Que je ne comprends pas le rapport entre ce que tu dis et ce que j'ai
dit. On peut discuter si ce que j'ai dit est logique -- il me paraît
logique, mais c'est mon avis. Mais la règle s'explique en deux lignes,
et n'a pas d'exceptions. Pour le C++, ça, c'est vraiment simple.

--
James Kanze GABI Software http://www.gabi-soft.fr
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