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
Vidéos High-Tech et Jeu Vidéo
Téléchargements
Vos réponses
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

James Kanze
Le #6567811
On May 7, 2:02 am, Mickaël Wolff
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é ?


C'est qu'une opération sur un objet n'en vaut pas la même
opération sur un autre, même si la « valeur » est la même.
L'exemple typique (mais bien simplifié), c'est un compte en
banque, et une valeur qu'on y vire ou prélève. Le compte a une
identité ; faire la virement sur une copie de l'objet, ou un
autre objet qui a par hazard la même valeur ne va pas. La valeur
qu'on y vire ou prélève, en revanche, n'a pas d'identité : 100
Euros, c'est 100 Euros, que ce soit une copie ou non.

(Dans la pratique, il arrive qu'on fasse les copies des objets
ayant une identité, dans la gestion des transactions, par
exemple. Mais chaque opération s'effectue quand même sur une
instance précise de l'objet. On ne copie ni n'affecte pas
librement, mais seulement dans une contexte bien définie -- où
il serait bien acceptable, voire préférable, de faire
l'affectation ou la copie au moyen d'une fonction explicite,
plutôt que de se servir de l'opérateur.)

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.


Les comparaisons ne présentent pas forcément tous les problèmes
des affectations. Si on suppose que le type dynamique fasse
partie de la « valeur », == renvoie faux dès que les types
sont différents (ce qui peut se faire simplement avec typeid).
Tandis que l'affectation n'est tout bonnement impossible entre
des types (dynamiques) différents.

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


Liskov Substitution Principle. En fait, oui, c'est un peu
l'interchangeabilité -- on doit pouvoir utiliser un Derived
partout ou un Base est démandé.

En fait, c'est lié à la notion du contrat : la base définit un
contrat, et tout dérivée doit le respecter. Et la
substitutabilité ne vaut, évidemment, que dans le cas où le
client respecte sa partie du contrat. (On peut, par exemple,
imaginer le cas où le code client ne marche qu'à cause des aléas
de l'implémentation de la base, et non à cause de quelque chose
de garantie par la base.) En gros, la dérivée ne doit pas
imposer des préconditions plus contraignantes, et doit garantir
des postconditions au moins aussi fortes que la base. Or, dans
ce que tu proposes, l'affectation d'une dérivée a la
précondition que ce qu'on affecte ait le type dérivée, et non
simplement base.

Mais comme d'habitude, ce n'est pas toujours aussi simple. Le
contrat peut être que l'affectation n'est admise que si
certaines conditions sont rempli. (Dans la bibliothèque
standard, par exemple, le contrat d'affectation d'un itérateur
exige que l'itérateur à droit de l'affectation n'ait pas une
valeur singulaire, c-à-d qu'il ne soit pas construit par le
constructeur par défaut.) Mais personnellement, je n'aime pas
trop que l'opérateur d'affectation ait des préconditions ; dans
de tels cas, je préfère de loin l'utilisation d'une fonction
nommée.

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.


Ce qui suggère (à moi, et sans connaître toutes les contraintes)
l'interdiction de l'affectation dans la classe de base, avec son
implémentation éventuellement dans les classes dérivées.

Un autre alternatif serait de traiter le message comme si
c'était un espèce d'entité, même s'il n'en est pas une
logiquement, et de n'en copier que les pointeurs. (Puisque
logiquement, un message ne contiendrait pas d'autres pointeurs,
et donc, des cycles sont par définition impossible, on pourrait
très bien se servir de boost::shared_ptr dans ce cas pour glaner
les cellules, si on n'a pas d'autre glaneur de cellules.) A
priori, j'imagine qu'un message, une fois initialisé, est
immutable ; qu'on transmet des copies (valeur) ou des pointeurs
(identité) ne change donc strictement rien, en dehors des
questions de la gestion de la mémoire.

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


Alexandre Abreu
Le #6578431
On May 7, 4:02 am, James Kanze

En gros, la dérivée ne doit pas
imposer des préconditions plus contraignantes, et doit garantir
des postconditions au moins aussi fortes que la base.


Je ne veux pas "hijacker" le sujet mais juste une remarque a propos de
cette affirmation.
C'est qquechose qu'on entend souvent et qui est dit et repete quand on
parle de contrat / interface
et qu'on evoque differentes implementations de ce contrat dans le
temps.

Par contre, c'est quelquechose qui m'a toujours plus ou moins derange
(bien que je l'utilise
de cette facon) car j'ai l'impression que tolerer des preconditions
moins contraignantes
et / ou des postconditions plus contraignantes brise le contrat d'une
certaine facon. Je n'ai
jamais encore pu accepter cette affirmation de but en blanc, peut etre
parce qu'il me manque
un element, je ne sais pas ...

Si je vais dans une chaine de restos qui exige que j'apporte mon vin,
et que j'ai l'habitude d'aller dans
un resto de cette chaine qui a choisi d'avoir une carte de vin et donc
de me lever la contrainte
d'aller chercher mon vin moi meme; puis que d'un coup je change de
resto
et que, contre toute attente, lui refuse ... me semble que c'est moi
qui ait brise le contrat initial non?
LSP se trouve plutot mal en point dans ce temps la,

Alex

James Kanze
Le #6582601
On May 8, 5:26 pm, Alexandre Abreu
On May 7, 4:02 am, James Kanze
En gros, la dérivée ne doit pas imposer des préconditions
plus contraignantes, et doit garantir des postconditions au
moins aussi fortes que la base.


Je ne veux pas "hijacker" le sujet mais juste une remarque a
propos de cette affirmation. C'est qquechose qu'on entend
souvent et qui est dit et repete quand on parle de contrat /
interface et qu'on evoque differentes implementations de ce
contrat dans le temps.

Par contre, c'est quelquechose qui m'a toujours plus ou moins
derange (bien que je l'utilise de cette facon) car j'ai
l'impression que tolerer des preconditions moins
contraignantes et / ou des postconditions plus contraignantes
brise le contrat d'une certaine facon. Je n'ai jamais encore
pu accepter cette affirmation de but en blanc, peut etre parce
qu'il me manque un element, je ne sais pas ...


Je suis en fait un peu de cet avis moi-même, que faire des
préconditions plus libérales ou des postconditions plus strictes
n'est pas vraiment dans le sens de la chose, au moins quand on
parle de l'aspect contractuel. Que la classe dérivée ne renvoie
dans la réalité qu'un sous-ensemble des valeurs permises par la
classe de base, ou qu'elle pourrait fonctionner dans les faits
avec des valeurs non-permises par la classe de base en entrée,
c'est tout à fait normal et même inévitable. Qu'elle en fasse
la garantie, lorsque la fonction est appelée à travers
l'interface de base, c'est une autre chose. L'interface de base
ne garantit que la contrat de base, quelque soit la classe
dérivée, et si, en tant qu'utilisateur, j'ai besoin du contrat
plus libéral que pourrait m'offrir la classe dérivée, à mon
avis, je dois le dire explicitement, en me servant de
l'interface de la classe dérivée, par exemple avec un
dynamic_cast (qui dit clairement qu'il me faut bien la dérivée,
et son contrat).

Du coup, quand j'implémente la programmation par contrat en C++,
je n'émule pas exactement Eiffel. Les fonctions publics de la
classe de base ne sont pas virtuelles ; elles vérifient le
contrat de la classe de base, et renvoient à des fonctions
virtuelles privées. Du coup, avec une interface de base, le
client se trouve contraint à respecter rigueureusement le
contrat de la classe de base. À mon avis, c'est mieux que ce que
fait l'Eiffel.

Mais bon, je donnais la définition de la LSP:-). (Et évidemment,
une classe qui conforme aux exigeances plus strictes ci-dessus
est aussi conforme à la LSP.)

Si je vais dans une chaine de restos qui exige que j'apporte
mon vin, et que j'ai l'habitude d'aller dans un resto de cette
chaine qui a choisi d'avoir une carte de vin et donc de me
lever la contrainte d'aller chercher mon vin moi meme; puis
que d'un coup je change de resto et que, contre toute attente,
lui refuse ... me semble que c'est moi qui ait brise le
contrat initial non? LSP se trouve plutot mal en point dans
ce temps la,


Je ne suis pas sûr de suivre l'exemple. Il me semble ici que tu
as bien une interface complètement différente : la fonction
choisirDeLaCarte() manque dans le deuxième cas.

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


Publicité
Poster une réponse
Anonyme