Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Un problème d'ordre d'évaluation des éléments d'une expression

7 réponses
Avatar
Marc G
Bonsoir,
Je viens de tomber sur un comportement inattendu de mon compilateur dans
l'évaluation des éléments d 'une expression.
La même expression évaluée à partir des types fondamentaux ou d'une classe
qui ne fait qu'encapsuler (pour reprendre la terminologie adéquate) ces
types donne des résultats différents :

1/ Expression sur types fondamentaux

double d=0.0,d1=10.0;
(d=d1)+=(d+=(d1+1.0));
ShowMessage(d); // affiche 21

2/ Classe qui ne fait que redéfinir "classiquement" les opérateurs utilisés
dans l'expression ci-dessus

//------------------------------
struct decimal {
explicit decimal(double value) : value_(value) {}
decimal(decimal const& x) : value_(x.value_) {}

double value_;

decimal operator+(decimal const& x) const
{
ShowMessage(AnsiString(value_)+"+"+AnsiString(x.value_));
return decimal(value_+x.value_);
}
decimal& operator+=(decimal const& x)
{
ShowMessage(AnsiString(value_)+"+="+AnsiString(x.value_));
value_+=x.value_;
return *this;
}
decimal& operator=(decimal const& x)
{
ShowMessage(AnsiString(value_)+"="+AnsiString(x.value_));
value_=x.value_;
return *this;
}

private :
decimal();
};
//------------------------------

J'évalue la même expression

decimal dec(0.0),dec1(10.0);
(dec=dec1)+=(dec+=(dec1+decimal(1)));
ShowMessage(dec.value_);

et le résultat est 20 !!!

Nota : j'ai laissé les ShowMessage dans le code (un copier/coller avec BCB
et vous pouvez vérifier vous même) , qui m'ont permis de suivre le
cheminement du compilateur.
Les calculs intermédiaires tracés sont
10+1
0+=11
11=10
10+=10, ce qui donne 20 au final

Si je rajoute dans la définition des opérateurs d'affectation le test
classique
if (&x!=this)
...
le résultat est alors 10 !

Je constate que le compilateur évalue dans ce cas en premier l'opérande
droite (au premier niveau) et semble utiliser une référence (la même à
droite qu'à gauche) vers la variable dec,
ce qui fait que la valeur affectée à dec dans l'opérande droite (soit 11)
est "écrasée". Ce comportement est normal.
Dans l'expression sur les types fondamentaux, je ne peux pas tracer le
chemin suivi...mais le résultat me semble incorrect !!

Qu'en pensez-vous ?
De plus, j'ai développé un compilateur : dois-je intégrer dans les
opérateurs d'affectation le test &x==this ?
Bon, je ne crois pas que les utilisateurs poussent jusqu'à des expressions
aussi tordues dont le sens n'apparaît pas évident, mais si il y a une règle,
autant la respecter.
Merci à vous
Marc

7 réponses

Avatar
Jean-Marc Bourguet
"Marc G" writes:

Bonsoir,
Je viens de tomber sur un comportement inattendu de mon compilateur dans
l'évaluation des éléments d 'une expression.
La même expression évaluée à partir des types fondamentaux ou d'une classe
qui ne fait qu'encapsuler (pour reprendre la terminologie adéquate) ces
types donne des résultats différents :

1/ Expression sur types fondamentaux

double d=0.0,d1.0;
(dÑ)+=(d+=(d1+1.0));
ShowMessage(d); // affiche 21



Comportement indéfini. N'importe quoi peut arriver.

2/ Classe qui ne fait que redéfinir "classiquement" les opérateurs utilisés
dans l'expression ci-dessus

//------------------------------
struct decimal {
explicit decimal(double value) : value_(value) {}
decimal(decimal const& x) : value_(x.value_) {}

double value_;

decimal operator+(decimal const& x) const
{
ShowMessage(AnsiString(value_)+"+"+AnsiString(x.value_));
return decimal(value_+x.value_);
}
decimal& operator+=(decimal const& x)
{
ShowMessage(AnsiString(value_)+"+="+AnsiString(x.value_));
value_+=x.value_;
return *this;
}
decimal& operator=(decimal const& x)
{
ShowMessage(AnsiString(value_)+"="+AnsiString(x.value_));
value_=x.value_;
return *this;
}

private :
decimal();
};
//------------------------------

J'évalue la même expression

decimal dec(0.0),dec1(10.0);
(decÞc1)+=(dec+=(dec1+decimal(1)));



L'ajout des fonctions rends ce comportement non spécifié. Tu vas avoir
soit

dec = dec1;
dec += +decimal(1);
dec += dec;

ou

dec += +decimal(1);
dec = dec1;
dec += dec;

mais tu es sûr que c'est l'un des deux.

ShowMessage(dec.value_);

et le résultat est 20 !!!

Nota : j'ai laissé les ShowMessage dans le code (un copier/coller avec BCB
et vous pouvez vérifier vous même) , qui m'ont permis de suivre le
cheminement du compilateur.
Les calculs intermédiaires tracés sont
10+1
0+
11
10+, ce qui donne 20 au final

Si je rajoute dans la définition des opérateurs d'affectation le test
classique
if (&x!=this)
...
le résultat est alors 10 !



Evidemment, tu ne fais pas la dernière ligne.

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 G
> Comportement indéfini. N'importe quoi peut arriver.


donc tu veux dire qu'il n'y a pas de règle définie ?
Je suis d'accord que l'expression n'a pas de "sens", mais ça m'étonne quand
même :-)
(comme je testais mon programme à partir des types fondamentaux et que j'ai
trouvé un écart, j'ai trouvé ça bizarre...)
le résultat est alors 10 !



Evidemment, tu ne fais pas la dernière ligne.


J'avais compris :-), et en fait ça confirme que ça n'a pas de sens.
Merci à toi
A +
Avatar
Fabien LE LEZ
On Mon, 10 Aug 2009 20:42:41 +0200, "Marc G" :

double d=0.0;


double const d1.0; // Jamais modifié, autant préciser "const".
(dÑ)+=(d+=(d1+1.0));



Expression un peu compliquée. Tu appelles plusieurs opérateurs
sucessivement.

Explicitons un peu :

operator+= ( //1
operator= (d , d1) //2
,
operator+= (d , operator+(d1 , 1.0)) //3
)

Pour chaque appel de fonction, les arguments sont évalués séparément,
dans un ordre indéfini. Le compilateur a le droit de faire ce qu'il
veut.

Les deux arguments de operator+= (//3) sont indépendants, donc le
comportement est prévisible : On ajoute (d1+1.0) à d.
De même, (//2) tout seul ne pose pas de problème : on donne à d la
valeur de d1.

Le problème vient du fait que //2 peut être évalué avant, ou après,
//3.


Dans le premier cas, l'exécution est :

d= d1; // 2 // La "valeur de retour" est une référence sur d
=> d= 10.0;
d+= d1 + 1.0; // 3 // La "valeur de retour" est une référence sur d
=> d+= 10.0 + 1.0
=> d= 21.0
d+= d;
=> d= 22.0


Dans le second cas, l'exécution est :

d+= d1 + 1.0; // 3 // La "valeur de retour" est une référence sur d
=> d+= 10.0 + 1.0
=> d= 21.0
d= d1; // 2 // La "valeur de retour" est une référence sur d
=> d= 10.0;
d+= d;
=> d= 20.0

Bien entendu, tu ne peux pas savoir à l'avance lequel des deux cas le
compilo va choisir. C'est ce que la norme appelle "comportement
indéfini".

Note toutefois que l'auteur d'un compilo donné a le droit de définir
un ordre d'évaluation, et de le décrire dans sa doc. Mais si tu
utilises cette information, ton code n'est plus portable.
Avatar
Marc G
> Bien entendu, tu ne peux pas savoir à l'avance lequel des deux cas le
compilo va choisir. C'est ce que la norme appelle "comportement
indéfini".

Note toutefois que l'auteur d'un compilo donné a le droit de définir
un ordre d'évaluation, et de le décrire dans sa doc. Mais si tu
utilises cette information, ton code n'est plus portable.


merci pour tes explications très claires qui répondent exactement à ma
question...
Marc
mes excuses, j'ai posté avant sur le mail perso = un problème de proximité
de bouton et de fatigue :-(
Avatar
James Kanze
On Aug 10, 8:42 pm, "Marc G" wrote:

Je viens de tomber sur un comportement inattendu de mon
compilateur dans l'évaluation des éléments d 'une expression.
La même expression évaluée à partir des types fondamentaux ou
d'une classe qui ne fait qu'encapsuler (pour reprendre la
terminologie adéquate) ces types donne des résultats
différents :



L'ordre d'évaluation dans les expressions est à quelques
exceptions près non spécifié. Ce n'est pas rare même qu'il varie
selon le niveau d'optimisation, pour la même expression. Donc,
même avec quelque chose du genre :
x = f( a + b ) + g( c + d ) ;
(en supposant des types définis par utilisateur, pour qu'on ait
accès à l'opérateur), les seules garanties qu'on a en ce qui
concerne l'ordre, c'est que :
a + b sera appelé avant f,
c + d sera appelé avant g,
f et g seront appelé avant la dernière addition, et
la dernière addition sera appelée avant l'affectation.
Tout ordre qui respecte ces règles est permis.

(Et pour faire complet : les opérateurs qui imposent un ordre
sont : &&, ||, ?: et le virgule, *quand* c'est un opérateur.)

1/ Expression sur types fondamentaux



double d=0.0,d1.0;
(dÑ)+=(d+=(d1+1.0));



Ici, c'est même pire, parce que tu as un comportement indéfini.
Si une expression modifie un objet, il est interdit d'accéder à
l'objet autrement dans l'expression, *sauf* pour le lire, si
cette lecture est nécessaire pour établir la nouvelle valeur.
Enfin, comme ci-dessus, il y a des exceptions -- la norme en
fait parle des points de séquence, et la règle ne s'applique que
dans la mesure où il n'y a pas de point de séquence entre les
accès. Les opérateurs qui imposent un ordre sont aussi des
points de séquence, ainsi que l'appel ou le retour d'une
fonction. Mais les points de séquence n'impose pas un ordre
complet ; dans mon exemple ci-dessus, l'appel de f est bien un
point de séquence, ce qui garantit non seulement que la valeur
de a + b sera calculé avant l'appel (ce qui est clairement
nécessaire), mais aussi que des effets de bord ont eu lieu avant
l'appel de f. En revanche, ce point de séquence n'a aucun effet
sur c + d.

Du moment que c'est un comportement indéfini, évidemment, le
compilateur est libre de faire ce qu'il veut.

ShowMessage(d); // affiche 21



2/ Classe qui ne fait que redéfinir "classiquement" les opérateurs ut ilisés
dans l'expression ci-dessus



L'utilation d'une classe enlève le comportement indéfini
(puisque les modifications se trouvent en fait dans des
fonctions, dont l'appel et le retour sont des points de
séquence). L'ordre d'évaluation, en revanche, est toujours
non-spécifié. C-à-d que dans la pratique, il n'y a pas de
différence. (Formellement, le premier aurait pû reformatter ton
disque dur, ce qui n'est plus le cas ici. Pratiquement, en
revanche, la seule chose que tu risques, c'est que le résultat
ne soit pas ce que tu veux, à cause de l'indéterminisme de
l'ordre.)

[...]
Si je rajoute dans la définition des opérateurs d'affectation
le test classique
if (&x!=this)
...
le résultat est alors 10 !



C'est que tes fonctions sont en ligne, et que le compilateur
optimise différemment selon qu'elles ont un if ou non.

Et en passant, le test d'affectation sur soi-même est prèsqu'un
anti-patterne. Si c'est nécessaire (pas du tout le cas ici),
c'est en général que l'opérateur d'affectation ne fonctionne pas
correctement dans le cas des exceptions.

[...]
De plus, j'ai développé un compilateur : dois-je intégrer dans
les opérateurs d'affectation le test &x==this ?



Surtout pas. Comme j'ai dit, c'est un anti-patterne ; quelque
chose à éviter.

--
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
Merci beaucoup, j'ai tout compris.
En pratique, dans mon petit compilateur, pour une opération d'affectation, à
l'exécution j'ai fait
1/ évaluation de l'opérande droite (il me faut une VALEUR)
2/ détermination uniquement de l'opérande gauche : on sait à ce stade que
c'est une lvalue et la valeur m'importe peu, je veux juste l'"adresse" de
l'expression
(avec quelques subtilités si l'expression à "déterminer" comporte des
expressions qui doivent être évaluées - comme un appel de fonction)
=> dans l'expression simple (x=y)=z, l'affectation x=y n'est jamais
réalisée.
ça vous paraît correct ?
Avatar
James Kanze
On Aug 11, 11:37 am, "Marc G" wrote:
Merci beaucoup, j'ai tout compris.
En pratique, dans mon petit compilateur, pour une opération d'affectati on, à
l'exécution j'ai fait
1/ évaluation de l'opérande droite (il me faut une VALEUR)
2/ détermination uniquement de l'opérande gauche : on sait à ce sta de que
c'est une lvalue et la valeur m'importe peu, je veux juste l'"adresse" de
l'expression
(avec quelques subtilités si l'expression à "déterminer" comporte d es
expressions qui doivent être évaluées - comme un appel de fonction)
=> dans l'expression simple (x=y)=z, l'affectation x=y n'est jama is
réalisée.



Comme j'ai dit, l'ordre est complètement indifférent. Tu fais ce
qui t'est le plus simple. Et quant à "(x=y)=z", l'expression a
un comportement indéfini ; c'est donc complètement égal ce que
tu fais. (Si je reconnaissais le cas dans le compilateur,
j'émettrais un message d'erreur.)

ça vous paraît correct ?



Autant que n'importe quoi d'autre.

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