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:
Donc B* devient un A* et reste un A*.



Donc comment delete retrouve la taille de l'objet a
detruire et surtout en quoi declarer le destructeur
virtual resoud le probleme s'il a le comportement que tu
dis?



C'est le problème de l'implémentation. Je ne crois pas que
le destructeur virtuel ait grand chose à voir avec la taille
à libérere (la technique classique c'est quand même que
l'allocateur connait la taille de ses blocs) mais plus avec
l'exécution du destructeur de la classe la plus dérivée et
au passage à l'operateur delete du bon pointeur en cas
d'héritage multiple.

Mais bon, c'est vrai qu'on peut utiliser des opérateurs
delete qui prennent la taille aussi.

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

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



Je ne crois pas que tu puisses compiler du code qui aboutisse a un
appel de methode pure.





Je retire ce que j'ai dit, j'ai relu le DE 13.2.4.1...

a+, ld.


#include <new>
#include <iostream>

using namespace std;

struct A {
A() : a(1) {
cout << "A::A() n";
}
virtual void f1() {
cout << "A::f1()n"; f2();
}
virtual void f2() {
cout << "A::f2()n";
cout << "name = " << typeid(*this).name() << endl;
}
virtual ~A() {
cout << "A::~A()n"; f1();
}

void* operator new(size_t sz) {
cout << "A::new() sz = " << sz << endl;
return ::operator new(sz);
}
void operator delete(void *p, size_t sz) {
cout << "A::delete() sz = " << sz << endl;
::operator delete(p);
}

int a;
};

struct B : A {
B() : b(2) {
cout << "B::B()n";
}
void f1() {
cout << "B::f1()n"; f2();
}
void f2() {
cout << "B::f2()n";
cout << "name = " << typeid(*this).name() << endl;
}
~B() {
cout << "B::~B()n"; f1();
}

int b;
};

int main()
{
A *p = new B;

cout << "sizeof(A) = " << sizeof(A) << endl;
cout << "sizeof(B) = " << sizeof(B) << endl;
cout << "a = " << p->a
<< ", b = " << static_cast<B*>(p)->b << endl;

delete p;

return 0;
}





Avatar
Gabriel Dos Reis
Jean-Marc Bourguet writes:

| (Supersedes, pour corriger une typo dans une reference)
|
| 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/6 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.

Moi aussi, d'ailleurs cela fait partie de la liste des choses «
undefined behaviour » que je voudrais enlever.
Le problème de la spécification, c'est quand l'appel est indirect --
ce qui forcerait à mettre l'adresse dans les vtables. Alors au lieu de
faire une règle spéciale, ils ont mis undefined-behaviour.

-- Gaby
Avatar
Jean-Marc Bourguet
Gabriel Dos Reis writes:

| Je comprends que l'effet soint indefini quand la fonction n'est
| pas definie, mais si elle l'est ca me choque un peu.

Moi aussi, d'ailleurs cela fait partie de la liste des choses «
undefined behaviour » que je voudrais enlever. Le problème de la
spécification, c'est quand l'appel est indirect -- ce qui forcerait
à mettre l'adresse dans les vtables. Alors au lieu de faire une
règle spéciale, ils ont mis undefined-behaviour.


Mais c'est deja ce qu'il faut faire pour les fonctions virtuelles non
pures. A moins que je comprenne mal 12.7/3

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

ou qu'il y ait une autre clause qui m'a echappe?

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
drkm
writes:

drkm wrote in message
news:...

[...]
En fait, je pensais au fait de normaliser le fait que lorsqu'une
classe définit une fonction membre virtuelle, le compilo déclare
automatiquement virtuel le destructeur. Cela me semblerait
intéressant. Ne serait-ce que pour le destructeur généré par défaut.

Comme il me semble que c'est presqu'obligatoire, sauf cas très
particuliers, je trouve que ce devrait être le comportement par
défaut. Cette proposition a-t-elle déjà été faite ? Et si oui, quels
arguments ont-ils joué contre ?


Je crois qu'on en a parlé. En gros, l'argument contre, c'est qu'on
n'aime pas que l'ajoute et surtout la suppression d'une fonction
virtuelle change la sémantique du destructeur. Je doute que ça ait un
effet dans les programmes réels, mais j'avoue que l'idée ne me plaît pas
particulièrement non plus.


Ok. Je n'avais pas pensé à cet aspect.

Ce qui aurait pû être mieux, peut-être, c'est que lorsque la classe
définit une fonction membre virtuelle, le compilateur ne génère aucune
des fonctions qu'il génère normalement par défaut. Mais c'est bien trop
tard d'y penser maintenant.


C'eût effectivement été une solution intéressante.

--drkm


Avatar
Gabriel Dos Reis
Jean-Marc Bourguet writes:

| Gabriel Dos Reis writes:
|
| > | Je comprends que l'effet soint indefini quand la fonction n'est
| > | pas definie, mais si elle l'est ca me choque un peu.
| >
| > Moi aussi, d'ailleurs cela fait partie de la liste des choses «
| > undefined behaviour » que je voudrais enlever. Le problème de la
| > spécification, c'est quand l'appel est indirect -- ce qui forcerait
| > à mettre l'adresse dans les vtables. Alors au lieu de faire une
| > règle spéciale, ils ont mis undefined-behaviour.
|
| Mais c'est deja ce qu'il faut faire pour les fonctions virtuelles non
| pures. A moins que je comprenne mal 12.7/3
|
| 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, ...
|
| ou qu'il y ait une autre clause qui m'a echappe?

C'est parce que historiquement, les fonctions virtuelles pures n'ont
pas forcément d'entrées dans la vtable, dans tous les compilateurs.


Certains compilateurs affectent une adresse spéciale (je crois que GCC
par exemple fera avorter le programme au motif que tu as appelé une
fonction virtuelle pure -- et je crois que c'est quelque chose de
requis par l'ABI commune).

-- Gaby
Avatar
Gabriel Dos Reis
Jean-Marc Bourguet writes:

| Gabriel Dos Reis writes:
|
| > Certains compilateurs affectent une adresse spéciale (je crois que GCC
| > par exemple fera avorter le programme au motif que tu as appelé une
| > fonction virtuelle pure -- et je crois que c'est quelque chose de
| > requis par l'ABI commune).
|
| g++, icpc, como font échouer
|
| #include <iostream>
|
| class A {
| public:
| A() { g(); }
| void g() { f(); }
| virtual void f() = 0;
| };
|
| void A::f() {
| std::cout << "A::fn";
| }
|
| class B: public A {
| void f() { std::cout << "B::fn";}
| };
|
| int main() {
| B b;
| }
|
| avec qqch comme (message de como)
|
| C++ runtime abort: a pure virtual function was called
|
| mais si on enlève le =0, le programme fonctionne et affiche
|
| A::f
|
| pour les 3 compilateurs. Je suis un peu confu de n'avoir
| pas essayé plus tot. Mais bon, j'ai (ré?)appris une
| particularité supplémentaire des virtuelles pures.

Oui, mais le fond demeure que tu as a un point : s'ils mettent une
adresse spéciale, ils pourraient en profiter pour mettre une adresse
utile, cela de la fonction virtuelle pure. Après tout, être une classe
abstraite veut juste dire qu'on ne peut pas créer un objet complet de
cette classe -- pas qu'il faille tout une armada spéciale.

-- Gaby
Avatar
Jean-Marc Bourguet
Gabriel Dos Reis writes:

Certains compilateurs affectent une adresse spéciale (je crois que GCC
par exemple fera avorter le programme au motif que tu as appelé une
fonction virtuelle pure -- et je crois que c'est quelque chose de
requis par l'ABI commune).


g++, icpc, como font échouer

#include <iostream>

class A {
public:
A() { g(); }
void g() { f(); }
virtual void f() = 0;
};

void A::f() {
std::cout << "A::fn";
}

class B: public A {
void f() { std::cout << "B::fn";}
};

int main() {
B b;
}

avec qqch comme (message de como)

C++ runtime abort: a pure virtual function was called

mais si on enlève le =0, le programme fonctionne et affiche

A::f

pour les 3 compilateurs. Je suis un peu confu de n'avoir
pas essayé plus tot. Mais bon, j'ai (ré?)appris une
particularité supplémentaire des virtuelles pures.

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
K. Ahausse
"Jean-Marc Bourguet" a écrit dans le message de
news:
Gabriel Dos Reis writes:


#include <iostream>

class A {
public:
A() { g(); }
void g() { f(); }
virtual void f() = 0;
};

void A::f() {
std::cout << "A::fn";
}

class B: public A {
void f() { std::cout << "B::fn";}
};

int main() {
B b;
}

avec qqch comme (message de como)

C++ runtime abort: a pure virtual function was called

mais si on enlève le =0, le programme fonctionne et affiche

A::f

pour les 3 compilateurs. Je suis un peu confu de n'avoir
pas essayé plus tot. Mais bon, j'ai (ré?)appris une
particularité supplémentaire des virtuelles pures.


Au passage je me permet d'indiquer que la version donnée par Gabriel Dos
Reis, c'est à dire appeler la fonction virtuelle pure directement du
constructeur, donne l'impression de mieux passée.

class A {
public:
A() { f(); }
//void g() { f(); }
virtual void f() = 0;
};

[
....
]

Avec cette mise en garde de sa part : " la fonction est *définie*, mais le
code invoque un fonctionnement indéfini ", qui, pour moi, reste une énigme.

Avatar
kanze
"K. Ahausse" wrote in message
news:<41a339a6$0$7238$...
"Jean-Marc Bourguet" a écrit dans le message de
news:
"K. Ahausse" writes:

Je ne vois pas ce que les virtuelles pures viennent faire ici.


Oups, pardon. Je croyais qu'il était question de l'appel d'une
fonction virtuelle pure (définie dans la classe dérivée) à partir du
constructeur de la classe de base.

Pour citer, James Kanze : "La seule chose à se rappeler, c'est que
pendant l'execution d'un constructeur ou d'un destructeur de la class
C, le type dynamique est C. On ne tient pas compte du futur (dans le
cas d'un constructeur) ..."

Et donc : a la construction de la classe de base, la fonction
virtuelle pure n'étant pas encore définie, on ne peut faire appel à
elle.


Non. La seule chose à se rappeler, c'est que la résolution dynamique ne
trouve jamais une fonction pûre. Si donc l'appel démande une résolution
dymaique, et que dans le type dynamique de l'objet au moment de l'appel,
la fonction est déclarée pure, c'est une erreur -- un comportement
indéfini qui donne un core dump avec les compilateurs que j'utilise
d'habitude.

Ce qui ne veut pas dire que je ne peux pas appelé la fonction. Seulement
qu'il faut que je le fasse d'une façon à contourner la résolution
dynamique.

Ce sont des membres virtuels comme les autres a deux differences
pres:
- les descendants instanciables sont obliges de les supplanter
- on peut ne pas les definir (a l'exception du destructeur)

Tu confonds peut-etre avec l'autre partie de la discussion avec
Laurent ou j'indique qu'on peut arriver a cause de ces regles sur le
type dynamique durant l'execution des constructeurs et des
destructeurs a appeler un membre virtuel pur sans le faire de
maniere qualifiee (on peut naturellement le faire de maniere
qualifiee dans d'autres contextes).


Là je n'ai pas compris .


class B
{
public:
B() ;
virtual void f() = 0 ;
} ;

B::B()
{
f() ; // comportement indéfini, probablement core dump.
B::f() ; // légal, appelle B::f().
}

Ensuite, évidement, il y a la règle qui disent qu'il faut une définition
pour toute fonction utilisée. Une fonction virtuelle non pure est, par
définition, utilisée -- il lui faut donc toujours une définition. Une
fonction non virtuelle ou virtuelle pure, en revanche, n'est utilisée
que si elle est réelement appelée. C-à-d que dans le cas d'une fonction
virtuelle pure, au moins de contourner la résolution dynamique comme
ci-dessus, on n'a pas besoin d'une définition, de même qu'on n'a pas
besoin d'une fonction non virtuelle qu'on n'appelle pas.

Si on utilise une fonction, et qu'il n'y a pas exactement une
définition, c'est aussi un comportement indéfini. Dans ce cas-là, en
revanche, avec tous les compilateurs que j'ai jamais utilisé, on a une
erreur lors de l'édition des liens.

--
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:...
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,
- la résolution dynamique se fait en fonction du type dynamique, et
- le type dynamique lors de la construction ou la destruction, c'est
le type qu'on est en train de construire ou de detruire.

En fait, l'intérêt (enfin, un des intérêts) d'une fonction virtuelle
pûre, c'est que si on ne l'appelle qu'avec une résolution dynamique, on
n'est pas obligé a en fournir une définition. (Voir §3.2/2 : « A virtual
member function is used if it is not pure. » Et par la suite, qu'il faut
fournir une définition à toute fonction qui « is used ».)

Dans la pratique, au niveau de l'implémentation, lorsque le compilateur
génère le contenu du vtbl, il n'a pas de moyen à savoir si la fonction
est définie ou non. Si la fonction n'est pas pure, la phrase que j'ai
citée entre en jeu, et c'est à charge de l'utilisateur d'assurer qu'elle
est définie -- le compilateur peut donc y mettre l'adresse, que
l'éditeur de liens résoudra. Si la fonction est pure, en revanche, d'une
part, il se peut qu'elle n'est pas définie, et qu'en mettre l'adresse
dans le vtbl provoquera une erreur à l'édition de liens, et de l'autre
part, la résolution dynamique qui se base sur la vtbl ne doit pas la
trouver (sinon, le programme a un comportement indéfini). Le compilateur
doit donc mettre quelque chose d'autre dans le vtbl -- tous les
compilateurs que je connais mets l'adresse d'une fonction qui aborte le
programme.

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