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
kanze
Jean-Marc Bourguet wrote in message
news:...
Laurent Deniau writes:

Pour obtenir cet alias, il faut jouer a l'appenti sorcier avec des
reinterpret_cast, d'ou UB. Ni static_cast, ni dynamic_cast ne
permettent d'obtenir cet d'alias derive de this.


Les alias peuvent provenir de l'appelant. Avec de l'heritage multiple,
ce peut un pointeur sur une base deja contruite passe en parametre.
Pas d'UB ni de jeux dangereux, mais bien qqch qu'on peut vouloir faire
raisonnablement:

struct A {
virtual void f();
};

struct D_A {
virtual void f();
};

struct B {
B(A*p) { p->f(); }
};

struct F: public D_A, B {
F(): B(this) {} // this comme D_A est valide car construit avant
};

Eh non, l'appel virtuel dans B::B() est indefini!


Il doit y avoir une erreur dans ton code, parce que je n'y piège rien.
Tel qu'écrit ci-dessus, ça ne doit pas compiler -- le constructeur de B
veut un A*, et tu lui passe un D_A*, un pointeur à une classe qui n'a
rien à voir.

Si l'intention est que D_A dérive de A (chose que je crois), alors, le
comportement est bien indéfini, selon §3.8/5, point 2. Mais ça n'a pas
grand chose à voir avec la discussion en cour (et à mon avis, cette
restriction représente un défaut dans la norme -- il faut que le
compilateur sache à cet instant comment convertir this en D_A*, parce
qu'il a dû le faire correctement déjà pour appeler le constructeur de
D_A).

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


Avatar
kanze
Jean-Marc Bourguet wrote in message
news:...
Laurent Deniau writes:

Jean-Marc Bourguet wrote:
Laurent Deniau writes:

Jean-Marc Bourguet wrote:

Laurent Deniau writes: Pendant
l'execution du constructeur et du destructeur, l'objet est du
type de la classe construite (et non de la classe la plus
derivee). Appeler un membre virtuel sur this a un comportement
defini: la fonction sera celle de la classe construite ou de
l'ancetre la fournissant (donc c'est un probleme si elle est
pure). Appeler un membre virtuel a travers d'un alias de this qui
n'est pas du type construit ou d'un de ses descendants est
indefini.


Ok.

Pour obtenir cet alias, il faut jouer a l'appenti sorcier avec des
reinterpret_cast, d'ou UB. Ni static_cast, ni dynamic_cast ne
permettent d'obtenir cet d'alias derive de this.
Les alias peuvent provenir de l'appelant. Avec de l'heritage

multiple, ce peut un pointeur sur une base deja contruite passe en
parametre. Pas d'UB ni de jeux dangereux, mais bien qqch qu'on
peut vouloir faire raisonnablement:

struct A {
virtual void f();
};
struct D_A {
virtual void f();
};
struct B {
B(A*p) { p->f(); }
};
struct F: public D_A, B {
F(): B(this) {} // this comme D_A est valide car construit avant
};
Eh non, l'appel virtuel dans B::B() est indefini!


Pourquoi? p dans B::B() est de type effectif F* (mais this est de
type B*). p->f() vas donc invoquer legalement D_A::f() sur un this
de type F*. Ou vois-tu un alias de this en tant que B*?



Ce devient délirant. Le type de p est A*. Toujours.

p et this designent le meme objet (le F en cours de construction)


Pas vraiement. p designe la partie A du l'objet, this designe l'objet
complet.

Je crois que la norme n'est peut-être pas aussi clair que voulu ici (je
ne l'ai pas révérifiée), mais dans le constructeur de B, this designe
bien un objet de type B, et non l'objet de type F (qui n'existe pas
encore). Ce que designe p est moins clair, parce que selon la norme, la
conversion d'un pointeur à F (c-à-d le this dans le constructeur de F)
en un pointeur à un type de base a un comportement indéfini.

p n'est pas un B (ou une de ses bases) c'est donc un comportement
indefini.


Le comportement indéfini a lieu même avant -- à la conversion de this
(F*) en A*. Le comportement indéfini a lieu même si B::B ne déréference
pas le pointeur, par exemple, s'il le stocke dans l'objet pour une
utilisation plus tard.

Et en l'occurance, j'ai réelement eu des problèmes à cet égard, au moins
quand la classe de base était virtuelle (or que le compilateur a pû bien
résoudre le problème pour appeler le constructeur de la base virtuelle).

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





Avatar
kanze
Jean-Marc Bourguet wrote in message
news:...
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.


§3.8 :

1 The lifetime of an object is a runtime property of the object. The
lifetime of an object of type T begins when:

-- storage with the proper alignment and size for type T is
obtained, and

-- if T is a class type with a non-trivial constructor, the
constructor call has completed.

[...]

5 Before the lifetime of an object has started but after the storage
which the object will occupy has been allocated [...], any pointer
that refers to the storage location where the object will be or was
located may be used but only in limited ways. [...] If the object
will be or was of a non-POD class type, the program has undefined
behavior if:

[...]
-- the pointer is implicitely converted to a pointer to a base
class type, or
[...]

Alors, dans le cas ci-dessus : 1) this est un D*, un pointeur à un objet
avec un type de classe non POD, et 2) l'appel du constructeur de A le
convertit implicitement en pointeur à une base du type. C'est donc un
comportement indéfini.

En fait, je me démande aussi... Indépendamment du paramètre, il faut
bien passer le this au constructeur de base. Est-ce que ça ne compte pas
comme conversion implicite en pointeur à une base aussi ? Si oui, je
dirais qu'on a bien affaire à un défaut dans la norme.

Dans la pratique, je n'ai jamais eu de problème qu'avec des classes de
base virtuelles. En revanche, quand j'ai essayé le suivant :

class MyIStream : private virtual MyStreambuf, public std::istream
...

MyIStream::MyIStream()
: std::istream( this )
{
}

avec au moins un compilateur, le pointeur passé à std::istream n'était
pas valid, c-à-d qu'il ne containait pas l'adresse de l'objet
std::streambuf classe de base de MyStreambuf.

En ôtant la dérivation virtuelle, ça marche partout. Mais le
comportement reste indéfini selon la norme. (Note que la seule chose que
fait std::istream avec le pointeur, c'est de le passer vers sa classe de
base, pour à la fin le stocker dans une variable membre de std::ios. Pas
que ce soit garanti par la norme, mais dans la pratique, c'est comme ça
que ça se passe.)

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


Avatar
Laurent Deniau
wrote:
Laurent Deniau wrote in message
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 ?


Je parle de type effectif, ou dynamique comme tu le dis. Je ne savais
pas que les __vtbl des objets etaient mis a jour quand ils transitaient
a travers leurs ctors/dtor (est-ce que j'etais le seul a ne pas le
savoir?). J'ai donc insite sur ce fait.

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.


on peut le dire comme ca. mais la distinction:

this statique A* dynamique A*
et
this statique A* dynamique B*

n'etait evidente dans tes propos. J'ai plutot compris que tu disais que
le compilateur changeait son mode de resolution des appels virtuels
quand il entrait dans un ctor/dtor.

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.


c'est ce qui etait confus dans tes propositions de regles. La derniere
formulee simplement aurait suffit.

a+, ld.


Avatar
Laurent Deniau
wrote:
Jean-Marc Bourguet wrote in message
news:...

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.



§3.8 :

1 The lifetime of an object is a runtime property of the object. The
lifetime of an object of type T begins when:

-- storage with the proper alignment and size for type T is
obtained, and

-- if T is a class type with a non-trivial constructor, the
constructor call has completed.

[...]

5 Before the lifetime of an object has started but after the storage
which the object will occupy has been allocated [...], any pointer
that refers to the storage location where the object will be or was
located may be used but only in limited ways. [...] If the object
will be or was of a non-POD class type, the program has undefined
behavior if:

[...]
-- the pointer is implicitely converted to a pointer to a base
class type, or
[...]

Alors, dans le cas ci-dessus : 1) this est un D*, un pointeur à un objet
avec un type de classe non POD, et 2) l'appel du constructeur de A le
convertit implicitement en pointeur à une base du type. C'est donc un
comportement indéfini.

En fait, je me démande aussi... Indépendamment du paramètre, il faut
bien passer le this au constructeur de base. Est-ce que ça ne compte pas
comme conversion implicite en pointeur à une base aussi ? Si oui, je
dirais qu'on a bien affaire à un défaut dans la norme.

Dans la pratique, je n'ai jamais eu de problème qu'avec des classes de
base virtuelles. En revanche, quand j'ai essayé le suivant :

class MyIStream : private virtual MyStreambuf, public std::istream
...

MyIStream::MyIStream()
: std::istream( this )
{
}

avec au moins un compilateur, le pointeur passé à std::istream n'était
pas valid, c-à-d qu'il ne containait pas l'adresse de l'objet
std::streambuf classe de base de MyStreambuf.

En ôtant la dérivation virtuelle, ça marche partout. Mais le
comportement reste indéfini selon la norme. (Note que la seule chose que
fait std::istream avec le pointeur, c'est de le passer vers sa classe de
base, pour à la fin le stocker dans une variable membre de std::ios. Pas
que ce soit garanti par la norme, mais dans la pratique, c'est comme ça
que ça se passe.)


j'espere que tu n'as pas l'intension d'utiliser un GC un jour ;-)

a+, ld.



Avatar
kanze
Jean-Marc Bourguet wrote in message
news:...
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.


C'est UB.

Ce qu'on peut et qu'on ne peut pas faire avec un pointeur à un objet en
cours de construction est décrit assez en détail dans §3.8, et en
particulier dans la paragraphe 5. En particulier, on peut dire :

Comportement indéfini :

1.
struct B
{
B( B*p ) ;
virtual void f() ;
} ;

struct D : B
{
D::D() : B( this ) {}
virtual void f() ;
} ;

B::B( B*p ) { p->f() ; }

2.

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

struct D : B
{
D::D() : B( this ) {}
virtual void f() ;
} ;

B::B( D*p ) { p->f() ; }

3.

struct B
{
B( B* p ) ;
B* p ;
} ;

struct D
{
D::D() : B( this ) {}
} ;

B::B( B* p ) : p( p ) {}

Défini :

4.

struct D ;
struct B
{
B( D* p ) ;
D* p ;
} ;

struct D
{
D::D() : B( this ) {}
} ;

B::B( D* p ) : p( p ) {}

Le cas 3 peut étonné -- c'est un idiome assez répondu, je crois.

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




Avatar
kanze
Jean-Marc Bourguet wrote in message
news:...
writes:

Alors, avec quoi est-ce que tu proposes de la remplacer,


Les fonctions virtuelles pures sont des fonctions virtuelles comme les
autres sauf qu'on n'est pas oblige de les definir et qu'elles
empechent l'instanciation des classent qui en comportent.


Les fonctions virtuelles pures sont des fonctions virtuelles comme les
autres, sauf quand elles ne sont pas comme les autres. On n'est pas à
une différence près.

C'est un comportement indefini si elles devraient etre executees mais
qu'elles n'ont pas ete definies. Ca me semble simple, pas de regles
particulieres, pas de changement bizarres dans la recherche quand on
passe de virtuel pur a non pur.


Selon la norme, ce que tu décris est en effet le comportement de toute
fonction, membre ou non, virtuelle ou non. Dans la pratique, en
revanche, il y a un message d'erreur si j'oublie d'implémenter une
fonction non virtuelle pure.

Je comprends ta position, même si elle ne convainc à moitié. À vrai
dire, je crois que l'argument le plus fort (mais pas le seul) contre
elle, c'est l'inertie. Un des intérêts majeurs des fonctions virtuelles,
c'est de ne pas avoir à les implémenter, et les avantages d'une règle ou
d'une autre quand on les implémente ne semblent pas être assez
importants pour justifier un changement. La règle actuelle me plaît
assez, mais si elle était différente, je ne crois pas que j'essaierais
de la changer.

Le probleme qu'elles posent ne me semble pas plus complique pour les
editeurs de liens que celui des inline ou des templates.


C'est probablement moins complexe que export, oui:-). La règle actuelle
date d'une époque où les éditeurs de liens étaient plus simple.

Mais même aujourd'hui, pour faire une implémentation de qualité, il
faudrait bien que l'éditeur de liens sache quand une référence externe
est une entrée dans un vtbl pour une fonction pure virtuelle, et quand
non. Parce qu'on veut bien une erreur pour des externes non résolus dans
les autres cas.

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


Avatar
Jean-Marc Bourguet
writes:

Si l'intention est que D_A dérive de A (chose que je crois),


Exact.

alors, le comportement est bien indéfini, selon §3.8/5, point 2.
Mais ça n'a pas grand chose à voir avec la discussion en cour (et à
mon avis, cette restriction représente un défaut dans la norme -- il
faut que le compilateur sache à cet instant comment convertir this
en D_A*, parce qu'il a dû le faire correctement déjà pour appeler le
constructeur de D_A).


J'ai eu un doute mais n'ai pas ete voir :-(

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
Gabriel Dos Reis
writes:

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

Pour la n-ième fois, le type dynamique d el'objet ne change pas.

Un constructeur de A n'est pas un constructeur de B.

-- Gaby
Avatar
Gabriel Dos Reis
writes:

| Je comprends ta position, même si elle ne convainc à moitié. À vrai
| dire, je crois que l'argument le plus fort (mais pas le seul) contre
| elle, c'est l'inertie.

Si c'est vraiment l'argument le plus fort, alors il n'est pas vraiment
fort.

| Un des intérêts majeurs des fonctions virtuelles,
| c'est de ne pas avoir à les implémenter, et les avantages d'une règle ou

Non, là tu parles des fonctions virtuelles *pures* -- il faut toujours
implémenter virtuelles non-pures.

| d'une autre quand on les implémente ne semblent pas être assez
| importants pour justifier un changement. La règle actuelle me plaît
| assez, mais si elle était différente, je ne crois pas que j'essaierais
| de la changer.

Le comportement proposé ne demandera pas qu'on implémente une fonction
virtuelle pure, si elle n'est pas utilisée. C'est d'ailleurs ce que
dit la norme actuellement.

| > Le probleme qu'elles posent ne me semble pas plus complique pour les
| > editeurs de liens que celui des inline ou des templates.
|
| C'est probablement moins complexe que export, oui:-). La règle actuelle
| date d'une époque où les éditeurs de liens étaient plus simple.
|
| Mais même aujourd'hui, pour faire une implémentation de qualité, il
| faudrait bien que l'éditeur de liens sache quand une référence externe
| est une entrée dans un vtbl pour une fonction pure virtuelle, et quand
| non. Parce qu'on veut bien une erreur pour des externes non résolus dans
| les autres cas.

On parle de ce qui se passe en pratique, aujourd'hui.

-- Gaby