OVH Cloud OVH Cloud

Opérateur ?:

9 réponses
Avatar
Loïc Joly
Bonjour,

Dans le code suivant :

struct A {};
struct B : A{};
struct C : A{};

A a;
B b;
C c;

bool test;

int f()
{
test ? a : b; // Ok
test ? b : a; // Ok
test ? b : c; // Pas ok
}

Je me serait attendu dans le troisi=E8me cas =E0 ce que l'expression me
retourne une valeur de type A, or la norme semble dire que ce cas n'est
pas couvert. Quelqu'un en connait la motivation ?

Merci,

--=20
Lo=EFc

9 réponses

Avatar
Hamiral
Loïc Joly wrote:
Je me serait attendu dans le troisième cas à ce que l'expression me
retourne une valeur de type A, or la norme semble dire que ce cas n'est
pas couvert. Quelqu'un en connait la motivation ?

Merci,



Il doit manquer quelque chose dans ta question, parce que là ... rien
compris !
Déjà ta fonction f() est incomplète, y'a pas de return ...

--
Hamiral

Avatar
Fabien LE LEZ
On 3 May 2006 09:37:27 -0700, "Loïc Joly"
:

test ? a : b; // Ok
test ? b : a; // Ok


Tiens ? Je me serais attendu à ce que ces deux-là renvoient une
erreur.
Mais bon, dans les deux cas, un opérande peut être converti en le type
de l'autre opérande (attention : il s'agit alors vraisemblablement
d'une R-value), donc on peut comprendre que ça marche.


test ? b : c; // Pas ok
}

Je me serait attendu dans le troisième cas à ce que l'expression me
retourne une valeur de type A,


Pourquoi donc ?
À ma connaissance, un tel principe de "réduction au facteur commun"
n'existe pas en C++.

Le fonctionnement de l'opérateur ?: impose que les deux opérandes
soient de même type.
S'il est possible de convertir un des opérateurs dans le type de
l'autre, tout va bien. Sinon, le compilateur ne s'amuse pas à essayer
de deviner quel type utiliser. Heureusement, d'ailleurs, sinon bonjour
les surprises...

D'une manière générale, mieux vaut expliciter les conversions autant
que possible -- ne serait-ce que pour s'assurer qu'on a bien compris
ce qui se passe.

test ? a : b;
test ? A(b) : a;
test ? A(b) : A(c);

Avatar
Fabien LE LEZ
On Wed, 03 May 2006 21:43:35 +0200, Hamiral :

Il doit manquer quelque chose dans ta question


Le code est effectivement erroné, mais la question est claire : Loïc
s'attendait à ce que b et c dans "test ? b : c;" soient convertis en
des objets de type A pour satisfaire aux conditions d'utilisation de
l'opérateur ternaire.

Avatar
Hamiral
Fabien LE LEZ wrote:

On Wed, 03 May 2006 21:43:35 +0200, Hamiral :

Il doit manquer quelque chose dans ta question


Le code est effectivement erroné, mais la question est claire : Loïc
s'attendait à ce que b et c dans "test ? b : c;" soient convertis en
des objets de type A pour satisfaire aux conditions d'utilisation de
l'opérateur ternaire.


Bon, alors là je vais sérieusement avoir besoin de clarifications, car je
n'y comprends rien de rien ...

--
Hamiral


Avatar
Fabien LE LEZ
On Wed, 03 May 2006 22:45:26 +0200, Hamiral :

Bon, alors là je vais sérieusement avoir besoin de clarifications, car je
n'y comprends rien de rien ...


L'opérateur ternaire prend trois arguments, le premier étant un
booléen.

Dans l'expression
resultat = test ? a : b;
Si "test" vaut true, "a" est assigné à "resultat".
Sinon, "b" est assigné à "resultat".

Si a et b sont de même type, tout va bien.
Sinon (par exemple, a est un double, et b est un int), il y a un
problème : de quel type est l'expression "test ? a : b" ?

Il y a donc une règle qui dit que les deux derniers arguments de
l'opérateur ?: doivent être de même type.
Si ce n'est pas le cas d'emblée, on ruse avec des conversions
implicites.

Ici, a est de type A, et b est de type B, qui dérive de A.
On peut donc convertir b en un A, et on se retrouve avec deux
arguments de type A.
"test ? a : b" est donc équivalent à "test ? a : A(b)".

Le problème initial était l'expression "test ? b : c".
b est de type B
c est de type C
On ne peut pas convertir b en un objet de type C.
On ne peut pas non plus convertir c en un objet de type B.
Donc, on est coincé, et le compilo affiche une erreur.

Avatar
kanze
Loïc Joly wrote:

Dans le code suivant :

struct A {};
struct B : A{};
struct C : A{};

A a;
B b;
C c;

bool test;

int f()
{
test ? a : b; // Ok
test ? b : a; // Ok
test ? b : c; // Pas ok
}

Je me serait attendu dans le troisième cas à ce que
l'expression me retourne une valeur de type A, or la norme
semble dire que ce cas n'est pas couvert. Quelqu'un en connait
la motivation ?


Probablement parce qu'on craignait que dans le cas général, il y
aurait trop de possibilités. Imagine, par exemple, « test ? &b :
&c » ; est-ce qu'il faudrait aussi considérer void* ?

Note qu'en ce qui concerne les pointeurs, les mêmes règles
s'appliquent aux comparaisons : « a == b » et « a == c » so nt
permis, mais non « b == c ». Pour être cohérent, il faudrait
commencer par ajouter un « otherwise » dans §5.9/2. Et il ne
faut pas oublier qu'une classe peut avoir plusieurs bases.
Donnée :

struct A {} ; struct B : A{} ;
struct C : B {} ;
struct D : B {} ;

C c ;
D d ;

test ? c : d ;

Est-ce que le résultat doit être A& ou B& ?

Sinon :

struct A {} ; struct B {} ;
struct C : A, B {} ;
struct D : A, B {} ;

C c ;
D d ;

test ? c : d ;

Ambigu ?

Maintenant, les mêmes cas, mais avec « test ? C() : D() » (c-à-d
des non-lvalues) : on slice ? Même dans un cas comme :

B const& b = test ? C() : D() ;

Je crois qu'on pourrait y trouver des réponses, et qu'une
extension pour permettre ce genre de chose serait possible
(encore que dans le dernier cas ...). Mais ce n'est pas
évident ; il faudrait pas mal de travail, et personne ne l'a
fait. (Je crois que l'histoire du slicing risque d'être le plus
ardue. Imagine une expression du genre (test ? C() : D()).f() où
f() est une fonction virtuelle de B. C'est clair qu'on ne veut
pas slicer, mais comment formuler la règle alors ? Quel est le
type de l'expression ?)

--
James Kanze GABI Software
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
Jean-Marc Bourguet
"Loïc Joly" writes:

Bonjour,

Dans le code suivant :

struct A {};
struct B : A{};
struct C : A{};

A a;
B b;
C c;

bool test;

int f()
{
test ? a : b; // Ok
test ? b : a; // Ok
test ? b : c; // Pas ok
}

Je me serait attendu dans le troisième cas à ce que l'expression me
retourne une valeur de type A, or la norme semble dire que ce cas n'est
pas couvert. Quelqu'un en connait la motivation ?


Je n'en sais rien. Ca me semble un changement par rapport a l'ARM
(qui parle simplement d'amener a un type commun, sans preciser comment
il est determine).

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
James Kanze
Fabien LE LEZ wrote:
On Wed, 03 May 2006 22:45:26 +0200, Hamiral :


Bon, alors là je vais sérieusement avoir besoin de
clarifications, car je n'y comprends rien de rien ...



L'opérateur ternaire prend trois arguments, le premier étant
un booléen.


Dans l'expression
resultat = test ? a : b;
Si "test" vaut true, "a" est assigné à "resultat".
Sinon, "b" est assigné à "resultat".


Si a et b sont de même type, tout va bien.
Sinon (par exemple, a est un double, et b est un int), il y a un
problème : de quel type est l'expression "test ? a : b" ?


Il y a donc une règle qui dit que les deux derniers arguments
de l'opérateur ?: doivent être de même type.


Strictement parlant, il y a une règle qui remplit à peu près une
page de la norme. Une sommaire succincte serait qu'on essaie de
convertir une des expressions au type de l'autre -- que le type
qui en résulte sera toujours le type d'un des deux opérands. (Ça
se complique du fait qu'il y a aussi une histoire des lvalues,
et le fait qu'une des expressions peut être un throw, qui a le
type void.) Mais il n'y a pas d'essai de la part du compilateur
à trouver un type commun.

--
James Kanze
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
James Kanze
Fabien LE LEZ wrote:
On 3 May 2006 09:37:27 -0700, "Loïc Joly"
:


test ? a : b; // Ok
test ? b : a; // Ok



Tiens ? Je me serais attendu à ce que ces deux-là renvoient
une erreur.


Mais bon, dans les deux cas, un opérande peut être converti en
le type de l'autre opérande (attention : il s'agit alors
vraisemblablement d'une R-value), donc on peut comprendre que
ça marche.


Dans ce cas-ci, le résultat est bien un lvalue. La règle qui
s'applique ici est : « If E2 is an lvalue: E1 can be converted
to match E2 if E1 can be implicitly converted to the type
"reference to T2", subject to the constraint that in the
conversion the reference must bind directly to E1. »

Dans certains cas, il y a des questions en ce qui concerne la
durée de vie des temporaires :

A const& ca = a ;
A const& ca2 = test ? ca : B() ;

Ici, ca est bien un lvalue, de type A const&. L'expression B()
se convertit implicitement en A const& ; c'est donc légale, et
le résultat de l'expression entière est de type A const&. Ce qui
va bien pour initialiser ca2. Seulement, est-ce que c'est B()
qui sert à initialiser ca2 (ce qui prologerait la durée de vie
du temporaire), ou est-ce que B() initialise plutôt une
référence temporaire dans l'expression, qui ensuite sert à
initialiser ca2 ? Cette deuxième interprétation n'est pas très
utile, mais la première pose des problèmes réels
d'implémentation -- quand ca2 cesse d'exister, il faut appeler
le destructeur de façon conditionnelle, selon ce qui a été la
valeur de test lors de l'initialisation. (Et n'oublie pas que
les expressions peuvent être bien plus complexe, avec des ?:
embriquées.)

test ? b : c; // Pas ok
}



Je me serait attendu dans le troisième cas à ce que
l'expression me retourne une valeur de type A,



Pourquoi donc ?
À ma connaissance, un tel principe de "réduction au facteur commun"
n'existe pas en C++.


Le fonctionnement de l'opérateur ?: impose que les deux
opérandes soient de même type.


Pas du tout. Sinon, il ne faudrait pas une page entière de la
norme pour en donner les règles:-).

S'il est possible de convertir un des opérateurs dans le type
de l'autre, tout va bien. Sinon, le compilateur ne s'amuse pas
à essayer de deviner quel type utiliser. Heureusement,
d'ailleurs, sinon bonjour les surprises...


D'une manière générale, mieux vaut expliciter les conversions
autant que possible -- ne serait-ce que pour s'assurer qu'on a
bien compris ce qui se passe.


test ? a : b;
test ? A(b) : a;


Attention ! La signification de ces deux expressions n'est pas
du tout la même. La première est un lvalue (qui pourrait servir
à l'initialisation d'une référence non-const, par exemple) -- le
type dynamique du résultat pourrait aussi être B. La deuxième
non -- et le type qui en résulte est forcément un A. Fais un
essai : ajoute une fonction virtuelle f() dans A et dans les
classes dérivées, et essaie :
(test ? a : b ).f() ;
(test ? a : A(b)).f() ;
avec test faux.

test ? A(b) : A(c);


Plus utile :
test ? static_cast< A& >( b ) : static_cast< A& >( c )
(Techniquement, un seul des casts suffit. Mais j'aime
l'orthogonalité.)

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