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
Laurent Deniau wrote in message
news:<co73ak$p5m$...

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


Quel rapport ? D'abord, le GC fait partie de l'implémentation -- c'est
leur problème à le faire marcher. Mais aussi, le GC fonctionne à un
niveau plus bas. Il ne s'occupe pas des « classes dérivées » et
«Wclasses de base ». Et, concernant de tels pointeurs : « Such a pointer
refers to allocated storage, and using the pointer as if the pointer
were of type void*, is well-defined. »

--
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
news:<co73ak$p5m$...


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



Quel rapport ? D'abord, le GC fait partie de l'implémentation -- c'est


Parce que tu utilises un pointeur membre qui pointe sur une autre partie
de l'instance, le tout dans le meme objet (au sens storage du terme).

Les GC n'aiment pas ca.

a+, ld.


Avatar
drkm
writes:

4.

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

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


Je suppose qu'il manque le fait que D dérive de B. Et c'est permis,
la qualification supplémentaire du constructeur, dans la définition de
la classe ?

} ;

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


Dont une version pourrait utiliser le CRTP afin de connaître le type
dérivé :

template< class D >
struct B {
B( D * d ) : p( d ) { }
D * p ;
} ;

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

En fait, cela me rappelle une dicussion d'il y a un certain temps,
sur la légalité d'employer this dans la liste d'initialisation d'un
constructeur. Je me souviens avoir eu de nombreux échanges avec Gaby,
notemment à propos de 3.8/5, mais qu'il ne m'avait pas pleinement
convaincu de cette légalité.

Je n'ai pas le temps de me replonger dans la lecture de cette
discussion maintenant, mais si quelqu'un à une réponse à cette
question, cela m'intéresse.

--drkm

Avatar
drkm
Gabriel Dos Reis writes:

writes:

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


Si je comprend bien, tu dis que, que l'on soit dans le processus de
création d'un objet dérivé ou non, à partir du moment où l'on est dans
le destructeur, on sait que le type dynamique de l'objet est égal au
type statique. Car même si l'on est dans le processus de destruction
d'un objet dérivé, à ce moment là, les parties spécifiques de l'objet
dérivé ont été détruites, et l'on n'a plus qu'on objet du type
statique du destructeur dans lequel on se trouve. Dans un
destructeur, on connait exactement le type de l'objet que l'on et en
train de détruire.

Et vice-versa pour les constructeurs.

Exact ?

--drkm

Avatar
Gabriel Dos Reis
drkm writes:

| Gabriel Dos Reis writes:
|
| > writes:
|
| > | 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.
|
| Si je comprend bien, tu dis que, que l'on soit dans le processus de
| création d'un objet dérivé ou non, à partir du moment où l'on est dans
| le destructeur, on sait que le type dynamique de l'objet est égal au
| type statique. Car même si l'on est dans le processus de destruction
| d'un objet dérivé, à ce moment là, les parties spécifiques de l'objet
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| dérivé ont été détruites, et l'on n'a plus qu'on objet du type
| statique du destructeur dans lequel on se trouve. Dans un
^^^^^^^^^^^^^^^^^^^^^^^^
| destructeur, on connait exactement le type de l'objet que l'on et en
| train de détruire.
|
| Et vice-versa pour les constructeurs.
|
| Exact ?

Exact.

Un constructeur d'une classe de base construit le sous-objet de la
classe de base, et non l'objet complet le plus dérivé. Et le type
dynamique de ce sous-objet reste, tout au long de sa vie, le type
statique vu par le constructeur. Il n'y a jamais de changement (comme
James voudrait le faire croire).

-- Gaby
Avatar
Falk Tannhäuser
Gabriel Dos Reis wrote:

Un constructeur d'une classe de base construit le sous-objet de la
classe de base, et non l'objet complet le plus dérivé.


D'accord.

Et le type
dynamique de ce sous-objet reste, tout au long de sa vie, le type
statique vu par le constructeur. Il n'y a jamais de changement (comme
James voudrait le faire croire).


Qu'est-ce que tu entends par le "type dynamique d'un sous-objet" ?
La définition du "type dynamique" dans § 1.3.3 ne parle pas de sous-objets.
§ 1.8/2 introduit la notion du sous-objet, et dans § 4.10/3 on parle de
la conversion d'un pointeur sur une classe dérivée vers un pointeur
sur la classe de base - le résultat pointe alors sur le sous-objet
de classe de base dans l'objet de classe drivée. (C'est pareil pour
les références.)
Or, il me semble que l'on ne puisse pas faire de distinction entre
le type dynamique d'un sous-objet de classe de base et le type dynamique
de l'objet complet de classe drivée:
___________________________________________________
#include <iostream>
#include <ostream>
#include <cassert>

struct B* pB1;

struct B
{
B() { pB1 = this; pB1->foo(); } // appelle B::foo()
virtual void foo() { std::cout << "Basen"; }
virtual ~B() { pB1->foo(); } // appelle B::foo()
};

struct D : public B
{
D() { pB1->foo(); } // appelle D::foo()
virtual void foo() { std::cout << "Derivedn"; }
};

int main()
{
D d;
B* pB2 = &d; assert(pB2 == pB1);
pB1->foo(); // appelle D::foo()
return 0;
}
___________________________________________________
affiche bien
Base
Derived
Derived
Base
alors que pB1 pointe bien sur le sous-objet de classe
'B' dans l'objet 'd' de classe 'D' ; ledit sous-objet
a été construit par le constructeur 'B::B()' ; ses
types statique et dynamique ont été 'B' dans ledit
constructeur, mais son type dynamique devient 'D'
une fois l'objet complet construit (et même déjà
dans le constructeur 'D::D()'), puis il redevient
'B' dans le destructeur 'B::~B()'.
Pour moi il y a donc bien et bel changement du type
dynamique (qui sert à résoudre les appels virtuels).

Falk

Avatar
Gabriel Dos Reis
Falk Tannhäuser writes:

| Gabriel Dos Reis wrote:
|
| > Un constructeur d'une classe de base construit le sous-objet de la
| > classe de base, et non l'objet complet le plus dérivé.
|
| D'accord.
|
| > Et le type
| > dynamique de ce sous-objet reste, tout au long de sa vie, le type
| > statique vu par le constructeur. Il n'y a jamais de changement (comme
| > James voudrait le faire croire).
|
| Qu'est-ce que tu entends par le "type dynamique d'un sous-objet" ?

Cést une tautologie de ma part. C'est le type dynamique du
« sous-objet en tant qu'objet », i.e. le type que tu vois dans le
constructeur qui construit le sous-objet. C'est le type utilisé pour
le spécifier. Il ne change jamais, que ce soit pour le sous-objet ou
pour l'objet complet. C'est une des caractéristiques de C++ -- un
objet ne change pas de type.

| La définition du "type dynamique" dans § 1.3.3 ne parle pas de sous-objets.

Cela concerne l'objet le plus dérivé.

the type of the most derived object (1.8) to which the lvalue
denoted by an lvalue expression refers.

| § 1.8/2 introduit la notion du sous-objet, et dans § 4.10/3 on parle de
| la conversion d'un pointeur sur une classe dérivée vers un pointeur
| sur la classe de base - le résultat pointe alors sur le sous-objet
| de classe de base dans l'objet de classe drivée. (C'est pareil pour
| les références.)

Oui, mais où est le rapport ?

| Or, il me semble que l'on ne puisse pas faire de distinction entre
| le type dynamique d'un sous-objet de classe de base et le type dynamique
| de l'objet complet de classe drivée:

Bah si : regarde dans le constructeur qui construit ls sous-objet et
le constructeur qui construit l'objet complet.

| ___________________________________________________
| #include <iostream>
| #include <ostream>
| #include <cassert>
|
| struct B* pB1;
|
| struct B
| {
| B() { pB1 = this; pB1->foo(); } // appelle B::foo()
| virtual void foo() { std::cout << "Basen"; }
| virtual ~B() { pB1->foo(); } // appelle B::foo()
| };
|
| struct D : public B
| {
| D() { pB1->foo(); } // appelle D::foo()
| virtual void foo() { std::cout << "Derivedn"; }
| };
|
| int main()
| {
| D d;
| B* pB2 = &d; assert(pB2 == pB1);
| pB1->foo(); // appelle D::foo()
| return 0;
| }
| ___________________________________________________
| affiche bien
| Base
| Derived
| Derived
| Base

Comme il se doit.

| alors que pB1 pointe bien sur le sous-objet de classe
| 'B' dans l'objet 'd' de classe 'D' ; ledit sous-objet
| a été construit par le constructeur 'B::B()' ; ses
| types statique et dynamique ont été 'B' dans ledit

Et ils le restent. Tu confonds une chose ici : le type dynamique de
l'expression « *pB1 » (qui évolue avec la completion de la constrution
de l'objet "D" avec le type de l'objet "B")

| constructeur, mais son type dynamique devient 'D'

Non, pas du tout. Son type dynamique ne change pas. La différence que
tu vois est dûe au fait que l'objet complet ou l'objet le plus dérivé
qui contient ce sous-objet est construit quand tu invoques foo() dans
main() ; et donc la résolution de l'appel à la fonction virtuelle va
se faire selon cet objet complet. Mais le sous objet
"B" ne change pas de type dynamique. Heureusement, sinon
dynamic_cast<> n'a plus de sens !

| une fois l'objet complet construit (et même déjà
| dans le constructeur 'D::D()'), puis il redevient
| 'B' dans le destructeur 'B::~B()'.

C'est que tu n'as pas bien compris les fonctions virtuelles et leur
mécanisme d'appel. Une fonction virtuelle est une propriété de l'objet
le plus dérivé. La résolution d'appel d'une fonction virtuelle se
fait toujours en fonction du *type dynamique de l'objet complet ou le
plus dérivé* (si l'objet complet n'est pas valide).

§5.2.2/1
[...] The function called in a member function call is normally
selected according to the static type of the object expression
(clause 10), but if that function is virtual and is not specified
using a qualified-id then the function actually called will be the
final overrider (10.3) of the selected function in the dynamic type
of the *object expression* [Note: the dynamic type is the type of the
object pointed or referred to by the current value of the object
expression. 12.7 describes the behavior of virtual function calls
when the objectexpression refers to an object under construction or
destruction. ]

(l'emphase est mienne)

12.7/3
Member functions, including virtual functions (10.3), can be called
during construction or destruction (12.6.2). When a virtual function
is called directly or indirectly from a constructor (including from
the mem-initializer for a data member) or from a destructor, and the
object to which the call applies is the object under construction or
destruction, the function called is the one defined in the
constructor or destructor's own class or in one of its bases, but
not a function overriding it in a class derived from the constructor
or destructor ou le plus dérivés class, or overriding it in one of
the other base classes of the most derived object (1.8). If the
virtual function call uses an explicit class member access (5.2.5)
and the object-expression refers to the object under construction or
destruction but its type is neither the constructor or destructor's
own class or one of its bases, the result of the call is undefined.

Les deux laius §5.2.2/1 et §12.7/3 sont résumés par le principe que
c'est le dynamique de l'objet complet ou le plus dérivé.
(1) si l'objet complet est valide, évidemment c'est son type qui
est le type utilisé (le type dynamique en question) pour résoudre
l'appel ; autrement
(2) on est dans un constructeur ou destructeur, alors c'est le type de
l'objet le plus dérivé (forcément celui du constructeur ou du
destructeur) qui résoud l'appel.

Un objet ou sous-objet ne change pas de type -- et encore moins de
type dynamique.

| Pour moi il y a donc bien et bel changement du type
| dynamique (qui sert à résoudre les appels virtuels).

c'est au mieux une illusion ou aberration. Voir ci-haut.

-- Gaby
Avatar
Gabriel Dos Reis
Laurent Deniau writes:

[...]

| Meme si oopc ne fonctionne pas comme C++, tu peux regarder les schemas
| du modele objet qui donne une idee des liens entre objet, sous-objet
| et vtbl.
|
| http://cern.ch/Laurent.Deniau/html/oopc/oopc.html

Tu en fais une proposition d'entrée pour la fAQ ? Je suis trop fatigué
pour re-expliquer tout ça en détail, mais je veux bien écrire des
bouts.

-- Gaby
Avatar
drkm
Laurent Deniau writes:

- construction de l'objet D: dans D::D()
this est de type D* ET
this->__vtbl pointe sur la __vtbl de D ET
this->__vtbl_C pointe sur la __vtbl_C de D
__vtbl_C est une copie de la __vtbl de C dans la __vtbl de D
mais avec en plus les redefinitions des methodes virtuelles par D.


Je ne comprends pas bien l'introduction de __vtbl_C.

[...]

Meme si oopc ne fonctionne pas comme C++, tu peux regarder les schemas
du modele objet qui donne une idee des liens entre objet, sous-objet et
vtbl.

http://cern.ch/Laurent.Deniau/html/oopc/oopc.html


Je n'ai pas énormément de temps pour l'instant, je n'ai fait que le
survoler. Est-ce utile de prendre le temps de le lire maintenant, ou
vaut-il mieux attendre la mise en ligne prochaine d'OOC 2 ? De toute
façon, sa lecture est dans ma TODO list ;-).

--drkm

Avatar
Falk Tannhäuser
Gabriel Dos Reis wrote:
Falk Tannhäuser writes:

| alors que pB1 pointe bien sur le sous-objet de classe
| 'B' dans l'objet 'd' de classe 'D' ; ledit sous-objet
| a été construit par le constructeur 'B::B()' ; ses
| types statique et dynamique ont été 'B' dans ledit

Et ils le restent. Tu confonds une chose ici : le type dynamique de
l'expression « *pB1 » (qui évolue avec la completion de la constrution
de l'objet "D" avec le type de l'objet "B")

| constructeur, mais son type dynamique devient 'D'

Non, pas du tout. Son type dynamique ne change pas. La différence que
tu vois est dûe au fait que l'objet complet ou l'objet le plus dérivé
qui contient ce sous-objet est construit quand tu invoques foo() dans
main() ; et donc la résolution de l'appel à la fonction virtuelle va
se faire selon cet objet complet. Mais le sous objet
"B" ne change pas de type dynamique. Heureusement, sinon
dynamic_cast<> n'a plus de sens !


Pour résumer :

On est tous les deux d'accord que :
- pB1 pointe sur le sous-objet "B" dans l'objet "D",
- le type dynamique de *pB1 est "D" pendant la durée de vie de l'objet
complet "D", ainsi que dans le constructeur et le destructeur de D
(et "B" dans le constructeur et le destructeur de "B").

Notre désaccord porte sur le type dynamique du sous-objet "B" pendant
la durée de vie de l'objet complet "D" (ainsi que dans le constructeur
/ destructeur de "D") :
GDR : le type dynamique reste toujours "B", le type dynamique de *pB1
n'y a rien à voir,
FT : on ne peut pas parler du type dynamique du dit sous-objet "B"
indépendamment du type de l'objet complet, sauf pendant la phase
de construction / destruction lorsque ce dernier n'existe pas
encore / plus.

Grande question :
Comment un programme conforme peut-il examiner le type dynamique du
sous-objet "B" pendant la durée de vie de l'objet complet (puisque
cela d'après toi n'est pas possible en regardant le type dynamique
de *pB1, bien que ce pointeur pointe sur ledit sous-objet "B") ?

| une fois l'objet complet construit (et même déjà
| dans le constructeur 'D::D()'), puis il redevient
| 'B' dans le destructeur 'B::~B()'.

C'est que tu n'as pas bien compris les fonctions virtuelles et leur
mécanisme d'appel.


C'est bien possible :-) Cela ne m'empêche cependant pas de pouvoir
prédire quelle sera la fonction virtuelle appelée, y compris dans
les constructeurs / destructeurs, donc je m'en n'inquiète pas plus
que ça pour l'instant ...

Falk