OVH Cloud OVH Cloud

Variable de type enumeration

63 réponses
Avatar
candide
Bonjour,

La norme C90 dit

--------------------------- 8< ------------------------------------
Each enumerated type shall be compatible with an integer type; the
choice of type is implementation-defined.
--------------------------- >8 ------------------------------------

Ce qui n'est pas très clair c'est que par ailleurs :

--------------------------- 8< ------------------------------------
An identifier declared as an enumeration constant has type int.
--------------------------- >8 ------------------------------------

Je me pose donc la question de savoir si on peut utiliser sans risque
une variable de type énumération exactement comme s'il s'agissait d'une
variable de type int ? Je dis "sans risque" parce que formellement, je
crois que rien ne m'en empêche :

--------------------------- 8< ------------------------------------
A char, a short int, or an int bit-field, or their signed or
unsigned varieties, or an object that has enumeration type, may be
used in an expression wherever an int or unsigned int may be used.
--------------------------- >8 ------------------------------------

Si, par exemple

enum toto {A=34, B=51} t;

peut-on écrire

t=28;

(cela pourrait poser problème si l'on considère que les valeurs prises
par une variable de type enum toto sont parmi 34 et 51, cf. ma première
citation de la norme) ? Bon, en fait j'imagine que ça ne pose pas de
problème particulier mais je trouve que la norme n'est pas formelle sur
ce point. Par contre, pourrait-il y avoir des problèmes si on écrivait

t=-5;

?

Quelles précautions particulières prendre avec des variables de type
énumération ? Harbison & Steele recommandent une certaine prudence :


--------------------------- 8< ------------------------------------
We suggest that programmers treat enumerated types as different from
integers and not mix them in integer expressions without using casts.
--------------------------- >8 ------------------------------------

mais ils ne donnent aucun exemple illustrant les précautions à prendre.
Concrètement faut faire quoi ? Ainsi dans mon premier exemple ci-dessus,
dois-je écrire

t=(enum toto) 28;

?

Merci.

10 réponses

Avatar
espie
In article <4871f2a3$0$24368$,
candide wrote:
Marc Espie a écrit :
In article <487145fa$0$900$,
Wykaaa wrote:
- Les enum doivent être utilisés conjointement avec switch.



Marrant, j'ai tendance a bannir switch de mon code.



La liste s'allonge !



Que veux-tu, avec l'experience, plus on ecrit du code, plus on tombe sur
les memes erreurs. C'est bien plus simple de rester sur un sous-ensemble
bien defini et sans erreur.

Typiquement, c'est bien agreable de pouvoir faire du C99... Rien de plus
utile que d'avoir des fonctions inline (ce qui enleve les 9/10 des utilisations
deplaisantes de macro) ou des types entiers bien definis (au revoir la jungle
des INT32 et assimiles des annees 80).

La seule vraie difficulte, c'est d'eviter de definir son propre sous-langage,
avec ses idiosyncrasies qui font qu'on est tout seul a parler le langage
en question (comme DJB, par exemple).

Mais bon, comme je ne bosse pas tout seul, ca n'est pas un vrai probleme...
Avatar
Erwan David
Pierre Maurette écrivait :

ne doit pas importer du tout, comme pour le switch...case. On
s'imposera alors les règles:
- Laisser l'enum se démerder, donc ne pas utiliser le forçage de
valeurs.
- Ne jamais présumer ni même tenir compte de la valeur numérique d'une
variable du type de l'enum. A une exception, si on ne fait pas de
forçage, on peut utiliser l'ordre dans lequel on a initialisé l'enum
et donc faire des comparaisons entre variables du même type enum.
- Ne jamais utiliser autre chose que des termes de l'enum, donc jamais
de valeurs numériques, dans le switch...case, ou un tableau, si on
décide de l'indicer par l'enum. Pas de résultats d'opérations entre
des variables de type de l'enum, ces résultats n'étant d'ailleurs pas
dans l'enum à priori.



J'ai un contre exemple...

Une spécification de machine virtuelle donne les noms des opcodes et
leur valeur numérique. L'utilisation d'un enum en forçant les valeurs a
des avantages en terme de lisibilité, et de debug (certains debuggueurs
pas trop cons donneront la valeur "enum" de la variable et non la valeur
numérique).

Mais là aussi on est dans un cas de switch...case

--
Le travail n'est pas une bonne chose. Si ça l'était,
les riches l'auraient accaparé
Avatar
candide
Pierre Maurette a écrit :

Si, par exemple

enum toto {A4, BQ} t;

peut-on écrire

t(;



Oui et non.
Oui parce que ça compilera. Le seul risque est de passer pour un idiot à
la relecture, ou de faire sombrer le relecteur dans des abîmes de
perplexité s'il cherche à comprendre quelque chose.



Ma question c'était d'abord pour lever une sorte d'ambiguité figurant
dans la norme. En outre, et d'une façon générale, un code mal conçu au
départ ou inadapté à ce en quoi on veut le tranformer peut devoir subir
des transformations qui nient le bon sens donc il était toujours bon de
savoir ce qu'on a le DROIT de faire.

Bien entendu, le compilateur est à mon sens autorisé à warner, et un
lint quelconque à vous châtier.



Je n'ai jamais pratiqué lint (juste parce que je sens que la doc est mal
faite et que je vais y passer x jours) mais pour ce qui est de gcc, ceci
passe sans crier avec les options -Wextra -Wall -pedantic :

int main(void)
{
enum toto { A = 34, B = 51 } t = 28;

t++;

return 0;
}



Les valeurs négatives sont valides dans les enum.



Dans les enum ? c'est imprécis. Les constantes énumérés ou une variable
de ce type, ce n'est pas pareil, les constantes peuvent prendre des
valeurs négatives maintenant que se passe-t-il si le type entier avec
lequel le type énuméré est compatible est non signé ?


Oublier un peu la norme et la littérature et se fixer des règles de bons
sens. Les enum ne sont jamais indispensables, et directement
intéressantes essentiellement en liaison avec un switch...case (et à mon
avis c'est tout à fait réciproque). Et dans quelques cas déterminés par
le fait que la valeur sous-jacente des termes de l'enum ne doit pas
importer du tout, comme pour le switch...case.



Oui, c'est bien l'idée que je me fais d'un usage "normal" des enum.


- Ne jamais présumer ni même tenir compte de la valeur numérique d'une
variable du type de l'enum. A une exception, si on ne fait pas de
forçage, on peut utiliser l'ordre dans lequel on a initialisé l'enum et
donc faire des comparaisons entre variables du même type enum.
- Ne jamais utiliser autre chose que des termes de l'enum, donc jamais
de valeurs numériques, dans le switch...case, ou un tableau, si on
décide de l'indicer par l'enum. Pas de résultats d'opérations entre des
variables de type de l'enum, ces résultats n'étant d'ailleurs pas dans
l'enum à priori.



Là encore, ça correspond aussi à l'usage que je considèrerais comme
"normal". Finalement, on n'utilise donc pas la possibilité qui est
donnée de changer les valeur par défaut 0, 1, 2, ... des constantes
d'énumération.




Il y a un cas qui fait exception à toutes ces règles, et qui est
peut-être envisagé dans la norme. Ce sont les enum composées de valeurs
entières avec un seul bit allumé. On pourra s'autoriser une mise en OU,
pour ensuite faire une lecture par masquage par les éléments de l'enum.
Il faudra quand même être prudent, la taille exigée monte très vite.




C'est une construction qui t'est propre ou c'est largement pratiqué dans
certains domaines que tu connais ?
Avatar
candide
Wykaaa a écrit :

Donc un truc comme ça est fautif selon toi :


enum booleen {FAUX, VRAI} v1, v2=1-v1;


?



Tout à fait fautif en effet, d'autant plus que v1 n'est pas initialisé
explicitement... et que les variables v1 et v2 sont déclarées dans la
foulée du type...
C'est exactement le genre d'exemple de code que je sanctionne dans les
audits et chez mes étudiants.

Ta ligne ci-dessus est un condensé de tout ce qu'il ne faut pas écrire
en C...




En fait, c'est pas tout à fait ça que je voulais écrire, plutôt :


enum booleen {FAUX, VRAI} ;

enum booleen negation(enum booleen v)
{
return 1 - v;
}

c'est pas OK ça ?
Avatar
candide
Marc Boyer a écrit :

J'ai reçu le "C Programming: a modern approach" de K.N. KING,
et il paraît pas mal à première vue.




Oui, il a mis du temps à sortir, nous en avions parlé il y a trois ans :

http://groups.google.fr/group/fr.comp.lang.c/msg/f86e968187d195f5?hl=fr


Je n'ai pas la dernière édition (C99) mais avec le recul, d'un point de
vue de la qualité des explications, c'est quand même le moins mauvais
ouvrage sur le C que je connaisse (en plus, il est vivement recommandé
sur clc).
Avatar
Pierre Maurette
candide, le 07/07/2008 a écrit :
Wykaaa a écrit :

Donc un truc comme ça est fautif selon toi :


enum booleen {FAUX, VRAI} v1, v2=1-v1;


?



Tout à fait fautif en effet, d'autant plus que v1 n'est pas initialisé
explicitement... et que les variables v1 et v2 sont déclarées dans la
foulée du type...
C'est exactement le genre d'exemple de code que je sanctionne dans les
audits et chez mes étudiants.

Ta ligne ci-dessus est un condensé de tout ce qu'il ne faut pas écrire en
C...




En fait, c'est pas tout à fait ça que je voulais écrire, plutôt :


enum booleen {FAUX, VRAI} ;

enum booleen negation(enum booleen v)
{
return 1 - v;
}

c'est pas OK ça ?



Si vous voulez vraiment une enum et une fonction negation():

enum booleen {FAUX = (1 == 0), VRAI = !FAUX} ;

enum booleen negation(enum booleen v){
return !v;
}

et on peut ajouter:

enum booleen normalise(enum booleen v){
return !!v;
}

ainsi qu'une auto-contrainte interdisant les opérateurs arithmétiques.

--
Pierre Maurette
Avatar
Wykaaa
Marc Espie a écrit :
In article <4871ef49$0$934$,
Wykaaa wrote:
Bien sûr que si qu'on peut se passer de l'équivalence pointeur/tableau
en C. Il suffit de programmer proprement.
C'est K&R qui font croire qu'on ne peut s'en passer...



Ca depend de ce que tu entends par `s'en passer'.

Ton code peut donner l'impression de ne pas s'en servir mais, de toutes
facons, des que tu ecris t[i], tu *utilises* cette equivalence, puisque
de toutes facons, par construction, c'est la facon dont cette construction
est evaluee: elle est indiscernable de *(t+i).



Certes. C'est la fameuse "arithmétique pointeur" chère à K&R dont ils
étaient si fière à leur début. L'équivalence t[i] avec *(t+i) montre en
fait que les tableaux n'existent pas en C (en poussant le raisonnement à
l'extrême).

C'est cable dans le langage. La seule facon de s'en passer, c'est de ne
pas du tout utiliser de tableaux.



Oui.

Sinon, tu fais des equivalences tableau/pointeur, le langage ne te laisse
pas le choix. Par exemple, void f(int *) et void f(int []) sont une seule
et unique signature de fonction. C'est meme pas une equivalence, le
compilateur ne fait pas la difference.


Bien sûr mais je pense qu'il est préférable d'écrire "void f(int[])"
plutôt que "f(int *)" quand le paramètre sera utilisé comme un tableau
dans la fonction.

Tu peux faire toutes les differences que tu voudras dans ta tete, tu auras
exactement zero aide du systeme cote typage pour s'assurer que tout va
bien.



Ça c'est sûr, hélas !

Pour le `il suffit de programmer proprement', c'est tout le probleme du C.
Tu as un langage tres faiblement type (autant dire pas du tout, j'exagere
a peine). Sur du code ecrit soi-meme, en faisant attention, on peut faire
des trucs qui fonctionnent. Sur du gros code, c'est tres dur d'etre sur
que tout est bon (et en plus, il y a plein de gorets qui ecrivent du code).



C'est ce que j'ai dit, autrement, dans une autre partie de ce fil.

Il y a quelques annees, j'avais experimente avec splint, un super-lint qui
se repose sur des annotations pour `rajouter' du typage et des controles
en C. Meme pour ecrire un programme relativement simple, c'etait complexe
d'avoir quelque chose de parfait (entre autres parce que des pans entiers
de la bibliotheque manquaient d'annotation). Ce systeme avait l'air fort
sympathique, en particulier parce qu'il permettait d'avoir un vrai typage
plus fort (en particulier, de virer les equivalences entre typedef et
de considerer les typedef comme des types abstraits).



Je ne connais pas splint mais effectivement ça semble sympathique.

A l'arrivee: beaucoup d'efforts, un resultat moyen, et la conviction
profonde que, de toutes facons, il n'y a rien a sauver. C'est impossible
de transformer le C en langage fortement type sans changer profondement
le langage. J'encourage ceux d'entre vous qui n'y croient pas a essayer.
Splint est disponible dans toutes les bonnes cremeries, et ca prend
juste quelques heures a prendre en main.



Faire un langage à typage fort n'est déjà pas forcément facile en soi
alors transformer un langage très faiblement typé comme le C en langage
à typage fort est tâche quasi impossible...

C'est un des trucs qui m'a fait considerer sous un jour nouveau le
boulot de Stroustrup: il a quand meme reussi le tour de force de rajouter
une couche objet *propre*, puis de la programmation generique, en gardant
99% de la semantique d'un langage qui est quand meme cradouille au
possible...



Même si C++ apparaît comme un langage très difficile à utiliser, même
pour des programmeurs chevronnés, il est sûr que Stroustrup a réussi un
joli "tour de force". Cependant, c'est au prix de quelques
idiosyncrasies comme "delete [] t" lorsque t est un tableau d'instances,
parce que, justement, il est impossible de distinguer un pointeur d'un
tableau (l'équivalence pointeur/tableau du C nous revient en pleine
gu**** !)
Avatar
Wykaaa
Marc Espie a écrit :
In article <48714c38$0$904$,
Wykaaa wrote:
- la structure de bloc qui est bancale (on ne peut pas définir de
fonction à l'intérieur d'une fonction). Il n'y a donc pas de notion de
fonction locale (il n'y a que la notion de fonction statique...)



Jamais compris a quoi ce genre de choses pouvait servir.
A la limite, j'aurais plus besoin de definir des pointeurs sur des
fonctions anonymes, ou de regrouper mes fonctions en classe, mais c'est
super-rare que j'ai besoin de fonctions locales pour organiser du code.



Des fonctions locales sont, en générale, des fonctions qui servent à
"implémenter" la fonction dans laquelle elles sont définies mais on ne
veut pas qu'un "utilisateur" les voit (encapsulation). En C++, on
pourrait les déclarer dans a partie privée d'une classe.

- le typedef qui introduit une liaison forte entre les niveaux lexical
et syntaxique et qui n'est pas du "vrai" typage mais seulement de
l'homonymie.



Il n'y a pas de typage en C. C'est tellement leger que c'en est inexistant.



Bon OK, mais quand même...

Un type enum est un ... type. #define n'indique aucun type pour un nom
et ce n'est pas une variable... (aucun contrôle ne peut être effectué
par le compilateur).



Allez, quelles sont les erreurs qu'enum permet de rattraper, cote typage ?
perso, je n'en vois aucune. Si c'est une enum, ca marche comme un entier,
et le compilo va peut-etre daigner donner un warning si on affecte un 1
a une enum... ca ne va pas tres loin.



Le type enum améliorent la lisibilité du code et, donc, sa
maintenabilité. Du point de vue sémantique, je suis d'accord que
l'apport est nul. C'est un simple confort syntaxique comme le switch qui
évite les cascades de "if...else" mais qui n'apporte rien à la
programmation structurée.
Avatar
Thierry B.
--{ candide a plopé ceci: }--

enum booleen {FAUX, VRAI} ;

enum booleen negation(enum booleen v)
{
return 1 - v;
}

c'est pas OK ça ?




Je trouve ça diaboliquement étrange, quand même :)


--
INT MAIN(INT ARGC,CHAR** ARGV)??<
IF((ARGC==1)??!??!(ARGC==2))??< PRINTF("GOOD??/n");
??>ELSE??< PRINTF("BAD??/n");
??> ??>
Avatar
Wykaaa
candide a écrit :
Wykaaa a écrit :

Donc un truc comme ça est fautif selon toi :


enum booleen {FAUX, VRAI} v1, v2=1-v1;


?



Tout à fait fautif en effet, d'autant plus que v1 n'est pas initialisé
explicitement... et que les variables v1 et v2 sont déclarées dans la
foulée du type...
C'est exactement le genre d'exemple de code que je sanctionne dans les
audits et chez mes étudiants.

Ta ligne ci-dessus est un condensé de tout ce qu'il ne faut pas écrire
en C...




En fait, c'est pas tout à fait ça que je voulais écrire, plutôt :


enum booleen {FAUX, VRAI} ;

enum booleen negation(enum booleen v)
{
return 1 - v;
}

c'est pas OK ça ?



Ben non car il vaut mieux écrire (tant qu'à faire...) :
enum booleen negation(enum booleen v)
{
return !v; /* ou return (enum booleen)(!(int)v) */
}
avec :
enum booleen {FAUX=0, VRAI=1} /* pour être sûr ! */