OVH Cloud OVH Cloud

operateur + comme fonction virtuelle pure dans une classe de base

33 réponses
Avatar
Marc
Bonjour, mon compilateur me refuse la définition de classe suivante :

class CTypeBase {

public :

CTypeBase() : _ismissing(true) {}
CTypeBase(const CTypeBase &x) : _ismissing(x._ismissing) {}

const bool& is_missing(void) const { return _ismissing;}
void set_missing(void) { _ismissing=true;}

// opérateurs de base redéfinis

virtual const CTypeBase operator+(const CTypeBase&) const=0;

protected :
private :

bool _ismissing;

};

Il m'indique juste que la classe CTypeBase ne peut contenir des fonctions
pures.
Ah bon !? pourquoi ?
ça n'a aucun sens pour moi de définir l'opérateur + dans la classe de base !
Merci de vos lumières

10 réponses

1 2 3 4
Avatar
kanze
"Marc" wrote in message
news:<417e7382$0$28190$...
Ben oui, mais non. Le polymorphisme ne marche qu'à travers des
références ou des pointeurs. Si l'on écrit
CTypeBase
ça veut dire que le type de l'objet est obligatoirement CTypeBase, et
non pas une classe dérivée ou une superclasse.


pourquoi ?


Parce que c'est comme ça que marche le langage.

En fait, en C++, quand tu écris que tu renvoies un CTypeBase, c'est bien
une instance que tu renvoies, et pour pouvoir renvoyer une instance, il
faut que le compilateur reserve quelque part de la mémoire pour mettre
cette instance. Mais pour reserver de la mémoire, il faut qu'il sache
combien, et ça dépend de la classe réele.

Pour renvoyer un objet polymorphique, il faut que toi, tu t'occupes
quelque part de l'allocation. Ce qui veut dire qu'en fait, le type que
tu renvoies soit 1) concret et 2) de taille fixe ; rien n'empêche, en
revanche, que ce type soit un handle à un type polymorphique.

La solution classique à ce genre de problème, c'est bien l'idiome de
lettre/envéloppe de Coplien. Tu auras donc quelque chose du genre :

class CType
{
public:
CType( CType* impl )
: myImpl( impl )
{
assert( impl != NULL ) ;
}

CType operator+( CType other ) const
{
return myImpl->operator+( other ) ;
}

protected:
CType() : myImpl( NULL ) {}

private:
CType* myImpl ;
} ;

Du coup, pour renvoyer un CTypeInt, on écrira quelque chose du genre :

return CType( new CTypeInt( valeur ) ) ;

Souvent, la classe de base connaît toutes les classes dérivées, et offre
soit des méthodes usine, soit des constructeurs spécialisés :

CType::Ctype( int valeur )
: myImpl( new CTypeInt( valeur ) )
{
}

C'est typiquement le cas où il faut implémenter aussi du double dispatch
(qui exige aussi que la classe de base connaisse toutes les classes
dérivées).

je peux tout à fait écrire virtual const ClasseDeBase operator+(const
ClasseDeBase &) const { return ClasseDerivee();} Ce qui m'intéresse,
c'est que ce soit le bon operator+ qui soit sélectionné.

Pour mon problème, j'ai trouvé une solution toute bête, dont voici le
code simplifié :

class CTypeInt;
class CTypeBase {

friend class CTypeInt;
public :

CTypeBase() : _ismissing(true) {}
CTypeBase(const CTypeBase &x) : _ismissing(x._ismissing) {}

const bool& is_missing(void) const { return _ismissing;}
void set_missing(void) { _ismissing=true;}

// opérateurs de base redéfinis

virtual const CTypeBase operator+(const CTypeBase&) const { return
CTypeBase();}

protected :

virtual const CTypeBase operator+(const CTypeInt&) const { return
CTypeBase();}

bool _ismissing;

};

class CTypeInt : public CTypeBase {

public :

CTypeInt() : CTypeBase() {}
CTypeInt(const CTypeInt &x) : CTypeBase(x),_value(x._value) {}

virtual const CTypeBase operator+(const CTypeBase&) const;

protected :

virtual const CTypeBase operator+(const CTypeInt&) const;

int _value;

};

En fait, je définis les opérateurs dans la classe de base, qui n'est
plus abstraite, même si je ne m'en servirai jamais.


Sauf qu'il servira. Parce que quand tu renvoies un CTypeInt( valeur )
dans la classe dérivée, il serait copié en CTypeBase, et par la suite,
tu n'auras qu'un CTypeBase.

J'ai trouvé sur Google une solution "esthétique" qui permet de
conserver la classe de base abstraite (avec des proxy), mais c'est une
usine à gaz qui rend le code incompréhensible et à mon avis peu
performant...


Tu vas payer une penalité de performance. Dans la pratique, le
polymorphisme veut dire que le compilateur ne connaît pas le type réel.
Et s'il ne connaît pas le type réel, il ne peut pas en reservé la
mémoire. Ce qui veut dire que les objets polymorphiques vont
obligatoirement être alloués dynamiquement, ce qui a un coût en
performance.

C'est comme ça. Toujours, et dans tous les langages. La seule
particularité de C++, ici, par rapport à des langages dits orientés
objet (Java, Smalltalk, etc.), c'est qu'en C++, ce coût est directement
visible. Et que tu ne le paies que si tu as besoin du polymorphisme.

Pour info, l'implémentation de operator+ dans la classe dérivée serait
un truc du genre :

const CTypeBase CTypeInt::operator +(const CTypeBase &x) const
// implémentation : CTypeInt+CTypeBase retourne CTypeInt
{
if (_ismissing || x._ismissing)
return CTypeInt();


Et le CTypeInt serait copié dans un CTypeBase, avec « slicing », et du
coup, tu n'auras plus qu'un CTypeBase.

else
return x.operator +(*this);
}

const CTypeBase CTypeInt::operator +(const CTypeInt &x) const
{
CTypeInt entier;
if (_ismissing || x._ismissing)
return entier;
entier._ismissingúlse;
entier._value=_value+x._value;
return entier;
}

J'espère que je reste compréhensible...


C'est un problème connu. Voir Coplien, qui implémente justement une
hièrarchie Number avec les techniques semblable : le double dispatch à
peu près comme tu le fais ici, et l'idiome lettre-envéloppe pour avoir
des « valeurs » polymorphiques.

--
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
"Christophe Lephay" wrote in message
news:<417ec6c7$0$3223$...
Marc wrote:

Tu as deux solutions pour résoudre ton problème. La première est le
double-dispatch.
[...]

La deuxième façon, plus modulaire, consiste à utiliser l'idiome
lettre-enveloppe qu'évoque Jean-Marc.


Ce sont deux solutions différentes à des problèmes différents. Et dans
son cas, il en auras probablement besoin des deux.

Le double dispatch sert dans le cas où la fonction à appeler (dans son
cas, operator+) dépend en fait des types dynamiques de deux objets. En
C++, la solution classique (et de loins la plus simple) exige en effet
que la classe de base connaisse toutes les classes dérivées -- mais il
en existe d'autres solutions, en général beaucoup plus lourdes et plus
coûteux en temps d'exécution.

L'idiome lettre-enveloppe sert à supporter le polymorphisme avec une
sémantique de valeur.

La hièrarchie à base de Number, dans Coplien, se sert des deux. Il y a
un exemple du double dispatch sans lettre-enveloppe dans ma class
GB_Option, pour la comparison des « matcher » -- voir les implémentation
de conflictsWith dans les GB_OptionMatcher (défini dans Option.lhh, dans
le composant Util/Process/CommandLine). Pour une variante de
lettre-enveloppe sans double dispatch, voir les itérateurs de la OSE
(http://www.dscpl.com.au/) -- j'ai aussi une classe Iterator quelque
part qui s'en sert pour rendre les itérateurs STL polymorphiques. (Il
faut que je le rétrouve, d'ailleurs -- il aurait dû être à ma site, mais
j'ai l'impression de l'avoir perdu.)

--
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
Marc
la réponse est prèsque toujours
l'idiome lettre-envéloppe.


Ce modèle est tout à fait adapté aux classes "volumineuses" et complexes.
Le problème est qu'il n'est pas très performant en terme de vitesse
d'exécution.
En effet dans l'opérateur +, il faut en général
1) recopier l'opérande gauche
2) appeler += polymorphe
3) recopier une nouvelle fois le résultat pour renvoyer un pointeur
Conclusion : ça rame pas mal
Autre solution possible que j'ai implémentée :
retour par référence vers un tampon fixe dans operator +

dans le fichier de définition de la classe de base

CTypeBase *CTypeBase::mainbufferPtr=NULL; // static
//-------------------------------------------------------
CTypeBase& CTypeBase::operator+(const CTypeBase &x) const
//-------------------------------------------------------
{
if (mainbufferPtr!=this) tobuffer();
mainbufferPtr->operator +=(x);
return *mainbufferPtr;
}

dans le fichier de définition d'une classe dérivée

CTypeInt CTypeInt::typeint; // static
void CTypeInt::touffer(void) const
{
//mises à jour du buffer typeint
.....
CTypeBase::mainbufferPtr=&typeint;
}

tobuffer() est une fonction virtuelle (pure dans CTypeBase) qui permet à
jour la variable globale dans l'unité concernée et indique ensuite l'adresse
de cette variable globale.
Seul inconvénient : une donnée membre statique par classe dérivée.
Dans mon cas, cette implémentation s'avère 15 fois + rapide que le modèle
proxy...
Marc

Avatar
kanze
"Marc" wrote in message
news:<417fd9dd$0$32450$...
Avant de te lancer dans le codage, je te conseille de faire des
recherches sur l'idiome enveloppe-lettre, mon exemple n'ayant eu
pour but que de t'expliquer l'idée générale (l'utiliser tel quel
serait source de problèmes)...


J'avais déjà implémenté ce modèle avec une classe "enveloppe"
externe... Ton modèle me pousse à chercher à mettre cette classe tout
au sommet de la hierarchie, ce qui facilite l'écriture et même
l'efficacité à ce qu'il me semble...


Ça ne change rien à l'efficacité ; ça ajoute en fait 4 ou 8 bytes
inutile à chaque objet de type dérivé, qui peut à la rigueur même nuire
à l'efficacité.

L'avantage du lettre-enveloppe, c'est que tu n'as pas besoin de
dupliquer l'interface dans la classe de handle. Le désavantage, c'est
que le compilateur ne râle pas si tu oublies de rédéfinir une des
fonctions virtuelles dans une classe dérivée.

--
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
"Marc" wrote in message
news:<4188b071$0$19673$...

la réponse est prèsque toujours l'idiome lettre-envéloppe.


Ce modèle est tout à fait adapté aux classes "volumineuses" et
complexes. Le problème est qu'il n'est pas très performant en terme de
vitesse d'exécution.


Par rapport à quoi ? Quels sont les altérnatifs ?

En effet dans l'opérateur +, il faut en général
1) recopier l'opérande gauche
2) appeler += polymorphe
3) recopier une nouvelle fois le résultat pour renvoyer un pointeur
Conclusion : ça rame pas mal
Autre solution possible que j'ai implémentée :
retour par référence vers un tampon fixe dans operator +

dans le fichier de définition de la classe de base

CTypeBase *CTypeBase::mainbufferPtr=NULL; // static
//-------------------------------------------------------
CTypeBase& CTypeBase::operator+(const CTypeBase &x) const
//-------------------------------------------------------
{
if (mainbufferPtr!=this) tobuffer();
mainbufferPtr->operator +=(x);
return *mainbufferPtr;
}

dans le fichier de définition d'une classe dérivée

CTypeInt CTypeInt::typeint; // static
void CTypeInt::touffer(void) const
{
//mises à jour du buffer typeint
.....
CTypeBase::mainbufferPtr=&typeint;
}


En somme, tu te sers d'un static pour le temporaire. Et qu'est-ce qui se
passe si tu as besoin de plus d'un temporaire à la fois ? Quelque chose
du genre :
(a + b) * (c + d)
par exemple. Tu vas utiliser le même mainbuffer pour le résultats de a +
b et les résultats de c + d ?

tobuffer() est une fonction virtuelle (pure dans CTypeBase) qui permet
à jour la variable globale dans l'unité concernée et indique ensuite
l'adresse de cette variable globale.
Seul inconvénient : une donnée membre statique par classe dérivée.
Dans mon cas, cette implémentation s'avère 15 fois + rapide que le
modèle proxy...


C'est souvent possible à rendre le code incorrect plus rapide que le
code correct.

Pour les classes dérivées les plus simples, c'est l'allocation qui
risque de bouffer le plus de temps. Si cette supposition est confermée
par le profiler, c'est rélativement simple d'utiliser un allocateur à
taille fixe propre à la classe.

--
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
Marc
En somme, tu te sers d'un static pour le temporaire. Et qu'est-ce qui se
passe si tu as besoin de plus d'un temporaire à la fois ? Quelque chose
du genre :
(a + b) * (c + d)
par exemple. Tu vas utiliser le même mainbuffer pour le résultats de a +
b et les résultats de c + d ?

En fait, j'ai 2 buffers, j'ai voulu simplifier pour ne pas compliquer le

code transmis et je me suis trompé...Et c'est cette version tout à fait
correcte qui va 15 fois plus vite que la version du type modèle proxy !


//-------------------------------------------------------
CTypeBase& CTypeBase::operator+(const CTypeBase &x) const
//-------------------------------------------------------
{
if (mainbufferPtr==&x) x.tosecondbuffer();
else secondbufferPtr=&x;
if (mainbufferPtr!=this) tomainbuffer();
mainbufferPtr->operator +=(*secondbufferPtr);
return *mainbufferPtr;
}

Marc
;-)

Avatar
Jean-Marc Bourguet
"Marc" writes:

En somme, tu te sers d'un static pour le temporaire. Et qu'est-ce qui se
passe si tu as besoin de plus d'un temporaire à la fois ? Quelque chose
du genre :
(a + b) * (c + d)
par exemple. Tu vas utiliser le même mainbuffer pour le résultats de a +
b et les résultats de c + d ?

En fait, j'ai 2 buffers,



void f(CTypeBase&, int nbBuf) {
if (nbBuf >= 0) {
f(a + b, nbBuf-1);
}
}

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
Jean-Marc Bourguet
"Marc" writes:

En somme, tu te sers d'un static pour le temporaire. Et qu'est-ce qui se
passe si tu as besoin de plus d'un temporaire à la fois ? Quelque chose
du genre :
(a + b) * (c + d)
par exemple. Tu vas utiliser le même mainbuffer pour le résultats de a +
b et les résultats de c + d ?

En fait, j'ai 2 buffers, j'ai voulu simplifier pour ne pas compliquer le

code transmis et je me suis trompé...Et c'est cette version tout à fait
correcte


essaie sur l'exemple de James.

qui va 15 fois plus vite que la version du type modèle proxy !

//-------------------------------------------------------
CTypeBase& CTypeBase::operator+(const CTypeBase &x) const
//-------------------------------------------------------
{
if (mainbufferPtr==&x) x.tosecondbuffer();
else secondbufferPtr=&x;
if (mainbufferPtr!=this) tomainbuffer();
mainbufferPtr->operator +=(*secondbufferPtr);
return *mainbufferPtr;
}


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
Marc
void f(CTypeBase&, int nbBuf) {
if (nbBuf >= 0) {
f(a + b, nbBuf-1);
}
}

Excuse-moi, mais je ne comprends pas bien ce que fais cette fonction...

Pour moi, 2 buffers suffisent, puisque les opérateurs n'ont jamais + de 2
opérandes (?: non surchargeable). Dans le cas de mon problème, le type de
base a été introduit pour implémenter les valeurs manquantes sur les types
fondamentaux. J'ai aussi dérivé un type Variant.
Et crois-moi, j'ai testé, la méthode avec buffers va beaucoup + vite qu'un
modèle proxy.
Comme le but est de faire des statistiques, la vitesse compte aussi pour
moi.

Avatar
Jean-Marc Bourguet
"Marc" writes:

void f(CTypeBase&, int nbBuf) {
if (nbBuf >= 0) {
f(a + b, nbBuf-1);
}
}

Excuse-moi, mais je ne comprends pas bien ce que fais cette

fonction...


Elle rend actifs nbBuf resultats de +. Donc nbBuf buffers ne
suffisent pas.

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


1 2 3 4