OVH Cloud OVH Cloud

pb d'opérateurs virtuels

17 réponses
Avatar
Marc G
Mon compilateur m'oblige à redéfinir l'opérateur virtuel = dans les classes
dérivées. Est-ce normal ?
Par contre, pour les autres opérateurs virtuels, ça passe très bien (ex :
operator +=())
L'imbrication de mes classes est un peu compliquée, mais j'ai
schématiquement :

Dans la classe de base
virtual CVariant& operator=(CVariant const& right) { /* */ return *this; }

Dans certaines classes dérivées, cette définition me va très bien, mais le
compilateur me dit que operator= n'est pas définit.
Un copier/coller de la définition de la classe de base et ça marche.
Mais pour operator+=, qui modifie lui aussi l'objet, ça marche. Et là je
vois pas pourquoi dans un cas et pas dans l'autre ...
Il y a une "règle" là dessus ?
Marc

7 réponses

1 2
Avatar
Marc G
et SQL n'est pas applicable ?
non, pas vraiment.

Je veux une approche mixte base de données classique (dans l'implémentation)
et approche "semi-objet" (pour l'utilisateur).
Et en plus SQL n'est pas vraiment destiné au traitement statistique. Comment
tu définis une variable multi-réponse (avec plusieurs modalités donc) ?
AMHA, il te faut une usine à gaz pour traiter ça...
Evidemment, je cherche une interface "conviviale" avec un clicodrome. Mais
comme tu sais, on ne peut pas tout faire avec la souris (par exemple
programmer en C++ :-)) et pour celui qui se donne la peine d'apprendre un
langage, s'il est bien défini, le clavier est souvent plus pratique.
Marc

Avatar
James Kanze
Marc G wrote:
Une première question : quel est le rÔle de ces objets dans ton
programme ?


Je développe un logiciel destiné à effectuer des traitements statis tiques
sur des données.
Pour cela, je cherche à implémenter un langage "simple" compréhensi ble par
quelqu'un qui ne fait pas de blocage sur l'ordinateur mais qui n'est pas
informaticien non plus :-) ! Dans le monde des enquêtes, c'est relative ment
courant...
(je sais pas si tu connais le langage SAS de SAS Institute, développé dans
les années 70 et qui n'a pas fondamentalement évolué dans sa struct ure. Dans
la version 8, il n'est toujours pas possible de définir des fonctions - c'est
quand même un concept bien pratique !- et il faut se débrouiller avec un
langage macro que mon intelligence normale ne me permet pas de comprendre.
En gros, j'écris "presque n'importe quoi" et je regarde ce que ça fai t !
C'est souvent lassant...). Tous mes clients qui utilisent SAS seraient pr êts
à changer pour n'importe quoi de plus simple, surtout qu'en général ils
veulent juste faire des choses basiques (genre tris croisés). Cette pet ite
intro rapide, c'est pour t'expliquer le contexte !
Les utilisateurs ont des tables de données importantes, au minimum 1.000
enregistrements et souvent + de 100.000 et il veulent
"traiter" les données.
Par exemple, dans une enquête ils ont demandé l'âge du chef de mé nage et les
revenus du ménage et ils veulent juste croiser des tranches d'âge ave c des
tranches de revenus.

tu vas écrire des trucs du genre (dans le style SAS):

tranche_age=6;
if age<60 then tranche_age=5;
if age<50 then tranche_age=4;
if age<40 then tranche_age=3;
if age<30 then tranche_age=2;
if age<20 then tranche_age=1;

Pour moi, la variable age est un "tableau" de 1000 entiers par exemple.


La variable tranche_age aussi ?

Tu vois qu'il s'agit de gros objets. C'est pourquoi je préfère que le
tableau réel soit alloué dans le tas, d'autant que j'ai prévu des
constructeurs qui récupèrent juste l'adresse de l'objet (en en prenant
possession ou pas).
En fait, j'ai défini une classe particulière
template <typename T> class CValue
pour implémenter les valeurs manquantes pour les types primaires -ou
autres - et c'est donc un "tableau" de 1000 CValue<int>.


OK.

Ma classe CValue répond aussi à une autre nécessité : définir l es quelques
méthodes qui doivent être implémentées par TOUS les objets suscep tibles
d'être mis dans un CVariant.


Deux problèmes : d'abord, ça melange deux conceptes -- à la
rigueur, Value pourrait hériter de la classe de base, mais il
n'y a pas de sens que tes autres objets héritent d'elle. Et de
l'autre, ce n'est pas comme ça qu'on spécifie les contraites
d'un template. Très souvent, même, on ne les spécifie qu'au
moyen de la documentation : pour être élément d'un std::vector,
par exemple, il faut que le type soit CopyConstructible et
Assignable, mais beaucoup d'implémentations ne le vérifie pas
explicitement. C'est juste de la documentation, jusqu'au moment
que le code utilise réelement un de ces conceptes. Et si on veut
le vérifier, on utilise quelque chose du genre
boost::concept_check (qui sert aussi dans l'implémentation de la
bibliothèque standard de g++) -- la métaprogrammation, en
somme. On n'impose pas une classe de base autrement sans
rélévance.

Et une méthode est essentielle, c'est
virtual CVariant& get_property(std::string const& property_name)


Mais dans la mesure où c'est appelée dans un template, on n'a
pas besoin de l'héritage.

Note que get_property retourne bien une référence et non un CVariant.
Ma classe CVariant a pour but de manipuler un ensemble d'objets
hiérarchisés, où les uns sont des propriétés des autres. C'est pas
exactement une classe CVariant "habituelle".

Par exemple, quand l'utilisateur écrit
int x=1;
l'interpréteur va créer un objet
CVariable<int> obj_x("x",1); // nom et valeur dans le constructeur
La valeur 1 va être stockée dans un objet CValue crée dans le tas e t pointé
par la donnée membre de CVariantSpecifique.
CVariable est une classe dérivée de CVariant qui redéfini get_prope rty
CVariable représente pour moi les variables "primaires", c'est à dire qui
n'ont pas de parent.
Les objets de cette classe possèdent par exemple les propriétés
name (ici "x")
typename (int/string/...)
missing
statut ->lecture seule O/N (pour les const)

si l'utilisateur tape
x.name
l'interpréteur appelle la méthode obj_x.get_property qui va retourner une
réference vers un CVariant qui contient le nom de la variable (en fait il
s'agira d'une référence vers un objet CConstante<U> dérivé de CVa riant et
qui est une donnée membre de CVariable<T>)
Pour comprendre le schéma fonctionnel, il y a une distinction entre la
valeur de l'objet, qui est stockée dans la classe CVariant Specifique e t le
type (au sens constante/variable/propriété) de l'objet qui est déte rminé par
le type de la classe dérivée de CVariant utilisé lors de la créat ion de
l'objet !
Mais TOUS les objets que je manipule par l'interpréteur (je veux dire
accessibles aux utilisateurs) définissent la méthode get_property (et
quelques autres). C'est donc aussi le cas de la valeur de l'objet, qui pe ut
d'ailleurs être elle-même un objet et avoir ou non des propriétés.
get_property gère aussi tous les messages d'erreurs liés à l'objet (lecture
seule/propriété inexistante...)


Je ne suis pas sûr d'avoir saisi tous les détails, mais je ne
vois pas où il y a un problème. Dans la structure que j'ai
proposée, Variant peut bien avoir une fonction get_property, qui
est implémentée dans la classe générique d'implémentation. Et il
n'y a pas de problème à dire qu'un des conceptes qu'utilise
ConcreteVariant, c'est le concepte HasProperty, ce qui signifie
que l'appelle get_property sur l'objet est légal, et qu'il
respecte certaines règles. (Alternativement, on peut dire qu'il
faut qu'il existe une fonction libre get_property( object ), qui
respecte ces règles. Ensuite, tu l'implémentes comme template
qui appelle la fonction sur l'objet. Ça laisse la possibilité
par la suite de fournir une spécialisation particulière pour des
types qui n'ont pas une telle fonction membre. Comme les types
de base.)

Au sujet de l'opérateur =
Tu as raison, dans la classe CVariantSpecific, il ne faut pas le redéfi nir.
Je l'avais fait mais c'est parfaitement inutile parce que CVariantSpecifi que
n'est jamais une lvalue car on ne manipule que des objets qui ne sont pas
directement "parent" (l'inverse de dérivé, je ne sais pas comment on dit) de
CVariantSpecific.
Pour les autres classes dérivées de CVariant par contre, il me faut le
redéfinir.


Ce que je crains toujours, c'est que tu sois en train de
melanger deux conceptes distincts dans une seule hiérarchie. Et
qu'un des conceptes ne correspond pas vraiment à une hiérarchie
d'héritage. À mon avis, tel que tu l'utilises, Variant ne doit
jamais servir de base que pour les implémentations d'un Variant,
c-à-d des classes qui sont cachées derrière la façade du
variant.

Pour que tu comprennes mieux, je te livre la définition simple des
constantes.

template<typename T>
class CConstante : public CVariant


Et c'est là où je décroche. Une Constante n'est pas un Variant,
parce qu'il a un type bien précis, qui ne peut pas varier.
Pourquoi veux-tu qu'il dérive de Variant.

{
typedef T Type;

public :

CConstante(Type const& t,std::string reference)
: CVariant(Empty()),
_reference(reference)
{ _variant=new CVariantSpecifique< T >(t); }

CConstante(CConstante const& x)
: CVariant(Empty()),
_reference(x._reference)
{ _variant=x._variant->duplicate();}

virtual ~CConstante() {}

//protected :

// la constante ne peut être une lvalue
virtual CVariant operator++(int)
{ throw CException(_reference+CCONSTANTE_NOLEFT_MSG);}
virtual CVariant operator--(int)
{ throw CException(_reference+CCONSTANTE_NOLEFT_MSG);}
virtual CVariant& operator=(CVariant const&)
{ throw CException(_reference+CCONSTANTE_NOLEFT_MSG);} // <- ici
par exemple
virtual CVariant& operator++()
{ throw CException(_reference+CCONSTANTE_NOLEFT_MSG);}
virtual CVariant& operator--()
{ throw CException(_reference+CCONSTANTE_NOLEFT_MSG);}
virtual CVariant& operator+=(CVariant const&)
{ throw CException(_reference+CCONSTANTE_NOLEFT_MSG);}
virtual CVariant& operator-=(CVariant const&)
{ throw CException(_reference+CCONSTANTE_NOLEFT_MSG);}
virtual CVariant& operator*=(CVariant const&)
{ throw CException(_reference+CCONSTANTE_NOLEFT_MSG);}
virtual CVariant& operator/=(CVariant const&)
{ throw CException(_reference+CCONSTANTE_NOLEFT_MSG);}

virtual void set_missing(void)
{ throw CException(_reference+CCONSTANTE_NOLEFT_MSG);}

virtual CVariant* duplicate(void) const { return new
CConstante(*this); }

std::string _reference;

// les constantes ne possèdent pas de propriétés
virtual CVariant& get_property(std::string const& property_name)
{ throw
CException(_reference+std::string(CCONSTANTE_NOPROPERTY_MSG)); }

virtual bool contains(CVariant const& right) const { return false;}
virtual bool read_only(void) const { return true;}
};


Jusqu'ici, je n'ai rien vu qui justifierait l'héritage et les
fonctions virtuelles. Et je ne vois pas pourquoi
l'implémentation de Constante utilise un Variant, ni pourquoi
les opérateurs opèrent sur des Variant. Une Constante, c'est une
Constante, et non un Variant. En revanche, on peut instantier
un Variant sur elle.

Quand aux opérateurs, il faudrait voir les contextes où ils
servent, mais régarde bien la référence de Coplien que j'ai
cité. Son exemple, c'est une hiérarchie Number, qui supporte
bien les opérateurs. Dans un cas comme ceci, je verais bien
que Constante ne supporte pas du tout l'opérateur ++. Variant
doit le supporter, avec une exception en cas où le type concret
ne le supporte pas, mais à mon avis, c'est de la
méta-programmation des templates dans ConcreteVariant qui doit
s'en occuper. Ou si tu ne te sens pas à l'aise avec de la
métaprogrammation (et dans ce cas-ci, c'est loin d'être trivial,
je crois), tu définis à la main une simple classe de traits qui
définit le comportement pour chaque type qui pourrait servir
d'instantiation de ConcreteVariant, quelque chose du genre :

template< typename T >
struct VariantTraits ;

template<>
struct VariantTraits< Constante >
{
static bool const hasIncr = false ;
// Semblable pour tous les autres opérateurs...
} ;

template< bool B >
struct Discriminator {} ;

Variant&
Variant::operator++() // virtuelle...
{
myImpl->operator++() ;
return *this ;
}

template< typename T >
ConcreteVariant< T >& // pas que ça importe...
ConcreteVariant< T >::operator++()
{
doIncr( Discriminator< VariantTraits< T >::hasIncr >() ) ;
return *this ;
}

template< typename T >
void
ConcreteVariant< T >::doIncr(
Discriminator< false > )
{
throw UnsupportedOperation() ;
}

template< typename T >
void
ConcreteVariant< T >::doIncr(
Discriminator< true > )
{
++ myValue ;
}

L'intérêt de la métaprogrammation, ici, c'est qu'il permettrait
de générer le VariantTraits automatiquement. Mais si je crois
voir à peu près comment faire pour hasIncr, c-à-d comment écrire
une expression de template telle qu'elle évalue true si le type
supporte ++, et false autrement, il me faudrait bien quelques
heures d'expérimentation pour le mettre au point, et pour en
être sûr qu'elle marche. Si tu veux apprendre ce genre de
chose : « C++ Templates : the Complete Guide », de
Vandevoorde et Josuttis. (Je te le conseille fort même si tu ne
veux pas t'attaquer à la métaprogrammation tout de suite. C'est
vraiment la référence en ce qui concerne les templates.) Et
ensuite, peut-être, « Modern C++ Design », d'Andrei
Alexandrescu -- je n'ai pas encore trouvé le temps de le lire
moi-même, mais d'après ce que dit tout le monde, et à juger
d'après d'autres choses que j'ai lu de Andrei, il vaudrait la
peine.

[...]
En fait mon interpréteur ne manipule que des objets CVariants.
Le seul moment où je dois manipuler d'autres classes dérivées, c'es t au
moment de la déclaration.


Mais alors, où est le problème. Si tu n'affectes jamais
d'instance d'une classe dérivée, comment peux-tu avoir un
problème dans la définition de l'opérateur d'affectation
d'elles.

S'agit-il d'une constante, variable et bien sûr de quel objet.
ex:
table a;
CVariable< CTable> obj_table("a"); par exemple.


Et pourquoi pas directement :
Variant< Variable< Table > > obj_table( Variable< Table >( "a" ) )
;
?

Mais comme tous les opérateurs peuvent être potentiellement utilisé s par les
CVariants, les objets disposent d'une données membres static unsigned i nt
qui mémorise les opérateurs légitimes sur l'objet. Pour les autres, une
exception est lancée.
Seule lourdeur, il me faut définir toutes les conversions 2 à 2.


Des conversions, ou plutôt des opérateurs binaires ? Le
problème, c'est qu'il faut dispatcher sur les deux types, que le
C++ ne supporte pas directement. Souvent, on peut se permettre
quelque chose du genre :

Variant
Variant::operator+( Variant const& other ) const
{
return myImpl->operator+( *other.myImpl ) ;
}

template< typename T >
Variant
ConcreteVariant< T >::operator+(
Variant const& other ) const
{
ConcreteVariant< T > const*
tmp
= dynamic_cast< ConcreteVariant< T > const* >( &other ) ;
if ( tmp == NULL ) {
throw MismatchedTypes() ;
}
return Variant( myValue + tmp->myValue ) ;
}

Mais ça suppose bien qu'on ne supporte jamais l'addition entre
deux types différents.

Mais bon, je suppose qu'il faut le faire de toutes façons, quelle que s oit
la méthode choisie. Pour les objets qui peuvent se convertir, il faut
l'écrire :-).
Et pour les autres, c'est vite dit (dans une petite macro par exemple pour
aller plus vite).


Encore plus vite (et plus propre) : une fonction templatée pour
la conversion, qui lève une exception. Ensuite, des
spécialisations explicites pour les cas que tu veux supporter.

Et dans ma classe de Contexte, j'aurai (c'est pas encore programmé) , s auf
surprise :-), que des pointeurs vers des CVariants.
J'espère que j'ai été clair et pas trop long...
Si tu vas jusqu'au bout Et que j'ai été clair, ça m'interesse de sa voir si
c'est un modèle sinon connu (on développe pas tous les modèles dans la vie
!), du moins intéressant et pertinant.


Pour certaines choses, ça ressemble encore fort à l'idiome
lettre/envéloppe de Coplien. Seulement, il ne faut pas melanger
les choses. Je crois que parfois, tu hérites dans le mauvais
sens ; que des classes comme Constante, Variable, etc. ne
doivent pas dériver de Variant, mais plutôt qu'elles doivent
être des classes concrètes, sans héritage, et qu'elles servent à
l'instantiation de ConcreteVariant seulement.

--
James Kanze (Gabi Software) email:
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
Christophe de Vienne
Une première question : quel est le rÔle de ces objets dans ton
programme ?


Je développe un logiciel destiné à effectuer des traitements statistiques
sur des données.
Pour cela, je cherche à implémenter un langage "simple" compréhensible par
quelqu'un qui ne fait pas de blocage sur l'ordinateur mais qui n'est pas
informaticien non plus :-) ! Dans le monde des enquêtes, c'est relativement
courant...


Est-ce que tu connais ça :

http://www.r-project.org/


A+

Christophe


Avatar
Marc G
template<typename T>
class CConstante : public CVariant

Et c'est là où je décroche. Une Constante n'est pas un Variant,
parce qu'il a un type bien précis, qui ne peut pas varier.
Pourquoi veux-tu qu'il dérive de Variant.


En fait, je crois qu'il y a incompréhension entre nous.
LE problème, c'est qu'une méthode qui retourne une propriété quelconque ne
peut retourner QU'UN VARIANT (ou une référence vers un variant, comme je le
fait), puisqu'on ne peut pas changer le type de retour.
D'OU l'idée d'implémenter les propriétés des objets visibles à l'utilisateur
elles-mêmes comme des variants, ce qui évite toute copie !
Dans l'exemple que je t'avais donné, une constante est un variant qui
n'autorise aucune opération modifiant sa valeur, comme tu peux le voir dans
le code que j'ai donné. Dans ce cas, on s'éloigne effectivement de l'idée de
variant : il s'agit plutôt d'utiliser CVariant comme classe de base pour
permettre de manipuler les objets avec un type visible unique (le type
CVariant). Mais la classe de base des objets
CConstante<T>/CVariable<T>/CProperty<T>...est bien la classe CVariant, telle
qu'elle serait définie pour implémenter des variants, à la différence près
que les opérateurs et quelques autres méthodes sont virtuels, au cas où il
ne s'agisse pas de CVariant !
C'est certain qu'il s'agit d'un mélange de 2 concepts et que cette méthode
n'est pertinente que si les objets utilisés dans les CVariant sont "assez"
volumineux (enfin pas les types primaires) puisque j'introduis des appels de
méthodes virtuelles. Mais c'est mon cas.

Ou si tu ne te sens pas à l'aise avec de la
métaprogrammation


c'est mon cas, hélas...
merci pour la classe de trait. Je m'en était sorti avec des directives
#define et des macros (certains n'aiment pas, mais c'est facile à
comprendre..et ça marche)
Mais je vais étudier ta méthode.

Enfin, merci encore pour ton aide.
Marc

Avatar
Marc G
Est-ce que tu connais ça :

http://www.r-project.org/


oui, je connais, mais ... j'ai pas trop regardé.
Et puis je suis malheureusement dans une approche commerciale (mais il n'y a
pas de mal à essayer de gagner sa croûte)
A +
Marc

Avatar
James Kanze
Marc G wrote:
template<typename T>
class CConstante : public CVariant

Et c'est là où je décroche. Une Constante n'est pas un Variant,
parce qu'il a un type bien précis, qui ne peut pas varier.
Pourquoi veux-tu qu'il dérive de Variant.


En fait, je crois qu'il y a incompréhension entre nous.
LE problème, c'est qu'une méthode qui retourne une propriété quel conque ne
peut retourner QU'UN VARIANT (ou une référence vers un variant, comme je le
fait), puisqu'on ne peut pas changer le type de retour.


Je le comprends bien. Mais je ne vois pas Constante dériver de
Variant pour le faire. Je vois plutôt un Variant qui contient la
Constante, au moyen de son ConcreteVariant.

D'OU l'idée d'implémenter les propriétés des objets visibles à l'utilisateur
elles-mêmes comme des variants, ce qui évite toute copie !


Mais sème de la confusion en ce qui concerne qui fait quoi.

Si la copie pose un problème de performance, il y a toujours
temps par la suite d'investiger des choses come COW (dans
ConcreteVariant, par exemple). Mais je ne modifierais pas la
conception pour des problèmes de performance que je n'ai pas
encore.

Dans l'exemple que je t'avais donné, une constante est un variant qui
n'autorise aucune opération modifiant sa valeur, comme tu peux le voir dans
le code que j'ai donné.


Et d'après son nom:-).

Dans ce cas, on s'éloigne effectivement de l'idée de
variant : il s'agit plutôt d'utiliser CVariant comme classe de base pour
permettre de manipuler les objets avec un type visible unique (le type
CVariant). Mais la classe de base des objets
CConstante<T>/CVariable<T>/CProperty<T>...est bien la classe CVariant, te lle
qu'elle serait définie pour implémenter des variants, à la différ ence près
que les opérateurs et quelques autres méthodes sont virtuels, au cas où il
ne s'agisse pas de CVariant !


Je crois toujours que tu embrouilles un peu les conceptes. D'un
côté tu as (ou tu dois avoir) des types (de valeur) bien
concrets, comme Constante, qui supportent les opérations qu'ils
supportent, et qui ne dérive de rien -- ce sont des valeurs
pûres. Ensuite, tu as Variant et ses dérivées qui emballent
chaque fois une instance de la classe concrète, et implémentent
le polymorphisme.

Ce qu'il faut bien saisir, c'est que pour que le polymorphisme
fonctionne, il faut une sémantique de référence, et pour que
l'affectation fonctionne, il faut une sémantique de valeur.
Variant et ces dérivées ne sert qu'à donner une sémantique de
valeur à des objets polymorphiques, qui eux contiennent des
valeurs, mais des types différents. Selon le cas, on pourrait
utiliser des types de base comme valeurs ; ce n'est pas rare
qu'un ConcreteVariant contient un type de base. Mais d'après ta
description, je ne crois pas que ça s'applique à ton
application.

C'est certain qu'il s'agit d'un mélange de 2 concepts et que cette mé thode
n'est pertinente que si les objets utilisés dans les CVariant sont "ass ez"
volumineux (enfin pas les types primaires) puisque j'introduis des appels de
méthodes virtuelles. Mais c'est mon cas.


Je crois qu'ici aussi, tu vois les choses à l'envers. Si la
conception dit qu'il faut du polymorphisme dynamique, tu te sers
des fonctions virtuelles. Que le type final soit un type
ultra-complexe, ou qu'il soit un int. On ne choisit pas entre
les fonctions virtuelles et les fonctions non-virtuelles sur la
base des problèmes de performance supposés ou soupçonnés.

En fait, l'expérience montre que si tu fais une bonne
conception et une implémentation où chaque décision est bien
encapsulée, tu arriveras à la fin à plus de performance que si
tu essaies d'optimiser dès le début.

Ou si tu ne te sens pas à l'aise avec de la
métaprogrammation


c'est mon cas, hélas...


Le mien, aussi, largement. J'en suis intrigué, mais à part
quelques petites expériences bien modestes, je ne l'ai pas
pratiqué, et je ne m'y connais pas très bien. Je sens ici un
problème qu'elle pourrait résoudre d'une façon élégante, mais je
ne sais pas trop comment m'y prendre.

--
James Kanze (GABI Software) email:
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 G
En fait, l'expérience montre que si tu fais une bonne
conception et une implémentation où chaque décision est bien
encapsulée, tu arriveras à la fin à plus de performance que si
tu essaies d'optimiser dès le début.


j'admets que ce n'est pas un conception très pure...pour un puriste ! :-)
et que c'est vrai que dès qu'un programme devient "un peu" volumineux, il
est parfois préférable de ne pas mélanger les concepts.
En tout cas, merci de ton aide et de tes conseils avisés.
Marc

1 2