GNT sans publicité, site mobile, fonctionnalitées exclusives...

Gestion de l'assignation consanguine

Le
Mickaël Wolff
Bonjour,

Si on a les trois classes suivantes :

class A
{
public:
virtual A & operator=(A const & rvalue) ;

/* */
} ;

class B0 : public A
{
public:
virtual A & operator=(B0 const & rvalue) ;
/* */
} ;

class B1 : public A
{
public:
virtual A & operator=(B1 const & rvalue) ;
/* */
} ;

Dans l'usage suivant :

B0 b0 ;
B1 b1 ;

A & b0_a = static_cast<A &>(b0) ;
b1 = b0_a ;

Dans la pratique, comment devrais-je gérer une telle affectation ?
Lever une exception car c'est un non sens ? Et surtout, comment je le
détecte dans l'affectation ? La solution la plus directe que je vois
dans le cas où on gère est un dynamic_cast dans B0::operator=(A const
&). J'avoue que je serais partisan de rendre l'opération silencieuse,
mais je risque de me surprendre, en perdant des information lors
d'affectations.

Que faites-vous pratiquement dans ce cas ?

Merci !
--
Mickaël Wolff aka Lupus Michaelis
http://lupusmic.org
Lire les 8 réponses

Vidéos High-Tech et Jeu Vidéo
Téléchargements
Vos réponses Page 1 / 2
Gagnez chaque mois un abonnement Premium avec GNT : Inscrivez-vous !
Trier par : date / pertinence
Sylvain SF
Le #6434771
Mickaël Wolff wrote on 30/04/2008 02:59:

class A {
public:
virtual A & operator=(A const & rvalue) ;
} ;

class B0 : public A {
public:
virtual A & operator=(B0 const & rvalue) ;
} ;

class B1 : public A {
public:
virtual A & operator=(B1 const & rvalue) ;
} ;

B0 b0 ;
B1 b1 ;
A & b0_a = static_cast<A &>(b0) ;
b1 = b0_a ;

Dans la pratique, comment devrais-je gérer une telle affectation ?


elle set impossible comme telle.
B1 définit un operateur = avec B1& en paramètre, pas un A&

Lever une exception car c'est un non sens ? Et surtout, comment je le
détecte dans l'affectation ?


si l'affectation est un non-sens, les operéateurs d'affectation
devraient être privés pour éviter ces non-sens.

La solution la plus directe que je vois dans le cas où on gère
est un dynamic_cast dans B0::operator=(A const &).


si en effet initialiser un B1 depuis un A a un sens (affectation des
attributs de base et déduction et/ou choix par défaut pour les membres
propres à B1) cet opérateur sur B0 et/ou B1 est requis.

J'avoue que je serais partisan de rendre l'opération silencieuse,
mais je risque de me surprendre, en perdant des information lors
d'affectations.


si la perte signifie que l'instance sera caduque, interdissez les
affectations; si les classes sont transposables (A = coordonnées,
B0 = cartesiennes, B1 = polaires) offrez ces opérateurs et pourquoi
pas un B0::operator= (B1 const&) (et vice-versa).

Sylvain.

James Kanze
Le #6435501
On Apr 30, 2:59 am, Mickaël Wolff
Si on a les trois classes suivantes :

class A
{
public:
virtual A & operator=(A const & rvalue) ;
/* ... */
} ;

class B0 : public A
{
public:
virtual A & operator=(B0 const & rvalue) ;
/* ... */
} ;

class B1 : public A
{
public:
virtual A & operator=(B1 const & rvalue) ;
/* ... */
} ;

Dans l'usage suivant :

B0 b0 ;
B1 b1 ;

A & b0_a = static_cast<A &>(b0) ;
b1 = b0_a ;

Dans la pratique, comment devrais-je gérer une telle
affectation ?


Dans la pratique, tu dois te démander si ça a un sens. Au moins
dans les applications que je fais, le polymorphisme s'applique
surtout à des objets ayant une identité, et donc, qui ne
supporte ni l'affectation ni la copie.

Sinon, il faut que tu définisses ce que ça signifie dans des cas
comme ceci.

Considère, par exemple, ton code. D'abord, virtuel ou non, tes
opérateurs d'affectation dans les classes dérivées ne
supplantent pas la fonction virtuelle dans la classe de base,
parce qu'ils n'ont pas les mêmes paramètres. Pour qu'il y a
supplantage, il faut définir un opérateur avec la même signature
dans les classes dérivées, c-à-d :

class B0 : public A {
{
public:
virtual B0& operator=( A const& other ) ;
// ...
} ;

(Que l'opérateur renvoie un B0& ou un A&, en revanche, n'a pas
d'importance, puisque le C++ supporte les types de retour
co-variants.)

Du coup, ton code ne passe pas le compilateur, parce que
l'opérateur d'affectation de b1 exige bien un B1 (l'opérateur
d'affectation dans B1 cache bien celui dans A), et tu lui passe
un A. (Pour les paramètres, il n'y a que le type statique qui
compte.)

Lever une exception car c'est un non sens ?


Est-ce un non sens, en voilà la question ? Dans les rares cas
où l'affectation d'un type polymorphique aient du sens, ce n'est
pas forcement un non sens. Encore, tout dépend. Je vois tout de
suite trois possibilités :

1. On n'accepte l'affectation qu'entre des types dynamiques
identiques. Mais dans ce cas-là, je me démande si on veut
réelement l'affectation (ou la polymorphisme) ; pour
certaines opérations (l'affectation), on n'est pas du tout
polymorphique, et pour d'autres si. C'est en tout cas une
violation flagrante du LSP.

S'il le faudrait, j'interdirais l'affectation au niveau de
la classe de base, et ne l'implémenterait qu'au niveau des
classes dérivées. Si le client veut affecter, il faut qu'il
sache avoir des instances de la même dérivée, et qu'il
utilise leur interface. (Par exemple, qu'il ait fait un
dynamic_cast avant, pour traiter un cas spécial.)

Dans la pratique, ce cas arrive facilement quand la classe
de base représente une qualité plutôt indépendante du rôle
de la classe, quelque chose comme PersistentObject. Dans ce
cas-là, en revanche, on ne traite avec la classe de base que
dans les fonctions qui gèrent cette qualité, ici par
exemple, les fonctions d'entrée/sortie. Et l'affectation se
fera bien toujours avec des classes dérivées.

2. Les classes dérivées représentent une famille, avec un état
abstrait commun. (Je ne peux malheureusement pas penser à de
bons exemples ; je crois que le cas ne s'est jamais
présenté dans mes applications.) Dans ce cas-là, il s'agit
d'écrire un opérateur d'affectation qui extrait l'état
commun, quelque soit le type à droit. Dans le cas le plus
général, il faudrait le « double dispatch », que la
fonction réelement appelée dépend à la fois du type de
l'objet à gauche, et de celui à droit.

Dans ce cas-ci, je ferais plutôt une fonction virtualle
assign() dans la classe de base, avec des surcharges pour
toutes les classes dérivées (implémentation classique du
double dispatch), ou qui cherche la fonction à appeler dans
un map (autre implémentation du double dispatch) ;
l'opérateur d'affectation se contentera d'appeler cette
fonction. Ou les opérateurs d'affectation, plutôt ; il en
faudrait au moins deux dans chaque classe dérivée -- un de
copie, pour empêcher que le compilateur en génère le sien,
et un qui prend une référence à la classe de base, pour
pouvoir affecter les d'autres types dérivés.

3. Enfin, on a réelement à faire avec une valeur polymorphique,
dont le type dynamique fasse partie de la valeur, et doit
être changé lors de l'affectation. Dans ce cas-ci, il faut
l'idiome du lettre/enveloppe. C-à-d que le type de base
contient un pointeur à l'objet réelement polymorphique, et y
renvoie tous les opérations. Et que tous les objets déclarés
soit du type de base, et que l'affectation fasse un clone de
l'objet contenu par l'objet à droit. Ça marche pas mal, mais
c'est un peu lourd, avec un clonage à chaque affectation ou
copie. (Dans le cas où les objets sont immutable, en dehors
de l'affectation, on peut éviter le clonage en se servant
d'un glaneur de cellules, ou éventuellement d'un comptage de
références. Ce qui le rend réelement utilisable, dans
beaucoup de cas.)

Et surtout, comment je le détecte dans l'affectation ?


La détection, c'est le problème la plus simple :

if ( typeid( *this ) != typeid( other ) ) ...

Savoir ce qu'il faut faire (c-à-d la conception) qui est plus
difficile.

La solution la plus directe que je vois dans le cas où on gère
est un dynamic_cast dans B0::operator=(A const &). J'avoue que
je serais partisan de rendre l'opération silencieuse, mais je
risque de me surprendre, en perdant des information lors
d'affectations.

Que faites-vous pratiquement dans ce cas ?


Je les évite, autant que possible. Dans la pratique, je trouve
qu'ils apparaissent assez rarement, et quand ils apparaissent,
ou bien, je me trouve dans le cas 1, ci-dessus, mais celui qui
veut affecter sait toujours très bien le type (suite à un
dynamic_cast au retour de la lecture, par exemple), et le
problème ne se pose pas réelement, ou je me trouve dans le cas
3, mais avec des objets immutable, et je m'en tire en simplement
affectant un pointeur (intelligent, genre boost::shared_ptr, si
je n'ai pas de glaneur de cellules).

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

Mickaël Wolff
Le #6437381
elle set impossible comme telle.
B1 définit un operateur = avec B1& en paramètre, pas un A&


En fait si. En fait, B0::operator=(B0 const &) empêche la création
d'un operator= automatique. Un effet de bord est donc l'appel de
A::operator=(A const &) à l'affectation d'un A. C'est du moins le
comportement que j'observe avec gcc 4.3.0, et il ne me parait pas aberrant.

Lever une exception car c'est un non sens ? Et surtout, comment je le
détecte dans l'affectation ?


si l'affectation est un non-sens, les operéateurs d'affectation
devraient être privés pour éviter ces non-sens.


Effectivement, c'est une solution à envisager.

si la perte signifie que l'instance sera caduque, interdissez les
affectations; si les classes sont transposables (A = coordonnées,
B0 = cartesiennes, B1 = polaires) offrez ces opérateurs et pourquoi
pas un B0::operator= (B1 const&) (et vice-versa).


Merci pour ta réponse.

Je répondrai à James un peu plus tard, car sa réponse va me demander
un peu beaucoup de réflexions et d'essais avant de pouvoir apporter une
réponse pertinente.

--
Mickaël Wolff aka Lupus Michaelis
http://lupusmic.org


James Kanze
Le #6535421
On Apr 30, 2:14 pm, Mickaël Wolff
elle set impossible comme telle. B1 définit un operateur =
avec B1& en paramètre, pas un A&


En fait si. En fait, B0::operator=(B0 const &) empêche la
création d'un operator= automatique. Un effet de bord est donc
l'appel de A::operator=(A const &) à l'affectation d'un A.
C'est du moins le comportement que j'observe avec gcc 4.3.0,
et il ne me parait pas aberrant.


En fait, non. Tu ne trouves l'operator=( A const& ) que parce
que tu affectes à travers un A. Rappelons comment se passe les
choses :

-- Le compilateur recherche le nom (ici, operator=). Dans le
cas d'un opérateur, cette recherche est un peu spéciale,
parce qu'il considère à la fois des fonctions libres et des
fonctions membre de la classe. (Il fait deux recherches
plus ou moins indépendantes.) Mais dans le cas de
l'opérateur d'affectation, ça ne change rien, parce qu'on
n'a pas le droit d'en fournir un qui n'est pas membre. La
recherche du nom se fait selon les types statiques
(obligatoirement, puisque c'est tout ce que connaît le
compilateur, et s'arrête là où le nom est trouvé pour la
première fois. C-à-d que si on recherche un operator= avec à
gauche de l'affectation un B0, le compilateur va trouver
B0::operator=. Toujours, parce que cette fonction ne peut
pas ne pas exister. Et il s'arrête là ; il ne regarde pas
dans les classes de base.

-- Ensuite, il y a la résolution des surcharges, mais seulement
parmi les fonctions que le compilateur a trouvé avant. Donc,
dans le cas où l'expression à gauche de l'affectation a le
type B0, que parmi les operator= de B0. Si tu essaies
d'affecter un A (ou une expression dont le type statique est
A, indépendamment du type dynamique de l'objet), et que B0
n'a pas elle-même un operator= qui prend un A, le
compilateur doit te rejeter.

-- Enfin, seulement une fois la résolution du surcharge finie,
le compilateur considère si la fonction est virtuelle ou
non.

Donc, avec les classes que tu avais définies :

B0* pba = new B0 ;
B0* pbb = new B0 ;
A* paa = pb0a ;
A* pab = pb0b ;
*paa = *pab ; // légal, appelle A::operator=(A const&)
*paa = *pbb ; // légal, appelle A::operator=(A const&)
*pba = *pab ; // illégal
*pba = *pbb ; // légal, appelle B0::operator=(B0 const&)

Si, dans A, l'operator=(A const&) est virtuel, *et* il est
supplanté dans B0 (avec un operator=(A const&)), alors, les deux
premiers appels seront virtuels, et appelleront
B0::operator=( A const& ). Mais il faut bien les deux
conditions : et que la fonction en A soit virtuelle, et qu'il y
a un supplantage avec exactement les mêmes paramètres (et la
même const-ité) dans B0.

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


Mickaël Wolff
Le #6566921

Dans la pratique, tu dois te démander si ça a un sens. Au moins
dans les applications que je fais, le polymorphisme s'applique
surtout à des objets ayant une identité, et donc, qui ne
supporte ni l'affectation ni la copie.


Qu'est-ce que tu appelles une identité ?

class B0 : public A {
{
public:
virtual B0& operator=( A const& other ) ;
// ...
} ;


Oui, en fait j'ai mal posé mes exemples. Finalement, dans le cas où
j'avais besoin d'affectations et de comparaisons virtuelles pour me
libérer de la nature des objets.


(Que l'opérateur renvoie un B0& ou un A&, en revanche, n'a pas
d'importance, puisque le C++ supporte les types de retour
co-variants.)


Ça j'avais compris :)


1. On n'accepte l'affectation qu'entre des types dynamiques
identiques. Mais dans ce cas-là, je me démande si on veut
réelement l'affectation (ou la polymorphisme) ; pour
certaines opérations (l'affectation), on n'est pas du tout
polymorphique, et pour d'autres si. C'est en tout cas une
violation flagrante du LSP.


LSP ~= interchangeabilité ?

S'il le faudrait, j'interdirais l'affectation au niveau de
la classe de base, et ne l'implémenterait qu'au niveau des
classes dérivées. Si le client veut affecter, il faut qu'il
sache avoir des instances de la même dérivée, et qu'il
utilise leur interface. (Par exemple, qu'il ait fait un
dynamic_cast avant, pour traiter un cas spécial.)


Ok.

Dans la pratique, ce cas arrive facilement quand la classe
de base représente une qualité plutôt indépendante du rôle
de la classe, quelque chose comme PersistentObject. Dans ce
cas-là, en revanche, on ne traite avec la classe de base que
dans les fonctions qui gèrent cette qualité, ici par
exemple, les fonctions d'entrée/sortie. Et l'affectation se
fera bien toujours avec des classes dérivées.


C'est certainement ce qui m'arrive. Pour être concret, j'ai une classe
abstraite message. Cette classe fournit des services de base (canal
d'adressage du message), et enveloppe les données transmises. J'ai
essayé de penser à un système générique pour pouvoir les envoyer de la
vue au contrôleur et du modèle à la vue.


3. Enfin, on a réelement à faire avec une valeur polymorphique,
dont le type dynamique fasse partie de la valeur, et doit
être changé lors de l'affectation. Dans ce cas-ci, il faut
l'idiome du lettre/enveloppe.


C'est marrant que tu parles d'enveloppe alors que j'envoie des messages.

La détection, c'est le problème la plus simple :

if ( typeid( *this ) != typeid( other ) ) ...


Certes.

Je les évite, autant que possible. Dans la pratique, je trouve
qu'ils apparaissent assez rarement, et quand ils apparaissent,
ou bien, je me trouve dans le cas 1, ci-dessus, mais celui qui
veut affecter sait toujours très bien le type (suite à un
dynamic_cast au retour de la lecture, par exemple), et le
problème ne se pose pas réelement, ou je me trouve dans le cas
3, mais avec des objets immutable, et je m'en tire en simplement
affectant un pointeur (intelligent, genre boost::shared_ptr, si
je n'ai pas de glaneur de cellules).


Merci pour toutes ces infos, ça m'a bien aidé à avancer. Même si je
suis de plus en plus convaincu d'avoir écrit une n-ième usine à gaz. :-/

Pas grave, je finirais par appliquer KISS ! Un jour.

--
Mickaël Wolff aka Lupus Michaelis
http://lupusmic.org

Publicité
Suivre les réponses
Poster une réponse
Anonyme