Type énuméré. Énumération énumération

24 février 2016 à 11h04

Conversion pratique des énumérations en chaînes en C++

  • Blog des solutions NIX,
  • C++
  • Programmation
  • Traduction

Les énumérations ont de nombreuses utilisations dans le développement. Par exemple, lors de la création de jeux, ils sont utilisés pour programmer des états de personnages ou des directions de mouvement possibles :

État d'énumération (Idle, Fidget, Walk, Scan, Attack) ; enum Direction (Nord, Sud, Est, Ouest);
C'est beaucoup plus pratique lorsque, lors du débogage, un message du type « State : Fidget » s'affiche dans la console au lieu de « State : 1 ». Il est aussi souvent nécessaire de sérialiser les énumérations au format JSON, YAML ou autre, et sous forme de valeurs de chaîne. Outre le fait que les chaînes sont plus faciles à percevoir que les nombres, leur utilisation dans le format de sérialisation augmente la résistance aux modifications des valeurs numériques des constantes d'énumération. Idéalement, « Fidget » devrait faire référence à Fidget même si une nouvelle constante est déclarée et que Fidget a une valeur autre que 1.

Malheureusement, en C++, il n'existe aucun moyen de convertir facilement les valeurs d'énumération en valeurs de chaîne et vice versa. Les développeurs sont donc obligés de recourir à diverses astuces qui nécessitent un certain support : des conversions codées en dur ou l'utilisation d'une syntaxe restrictive et disgracieuse, comme les macros X. Certains utilisent également des outils de construction pour la conversion automatique. Naturellement, cela ne fait que compliquer le processus de développement. Après tout, les énumérations ont leur propre syntaxe et sont stockées dans leurs propres fichiers d'entrée, ce qui ne facilite pas le travail des outils de construction dans les Makefiles ou les fichiers de projet.

Cependant, en utilisant C++, vous pouvez résoudre beaucoup plus facilement le problème de la conversion des énumérations en chaînes.

Il est possible d'éviter toutes les difficultés mentionnées et de générer des énumérations avec une réflexion complète en C++ pur. L'annonce ressemble à ceci :

BETTER_ENUM(État, entier, Inactif, Fidget, Marche, Scan, Attaque) BETTER_ENUM(Direction, entier, Nord, Sud, Est, Ouest)
Conseils d'utilisation :

État état = État :: Fidget ; state._to_string(); // "Fidget" std :: cout<< "state: " << state; // Пишет "state: Fidget" state = State::_from_string("Scan"); // State::Scan (3) // Применяется в switch, как и обычное перечисление. switch (state) { case State::Idle: // ... break; // ... }
Cela se fait à l'aide de plusieurs astuces liées au préprocesseur et au modèle. Nous en parlerons un peu à la fin de l'article.

En plus de la conversion en chaînes et inversement, ainsi que des E/S en flux, nous pouvons également parcourir les énumérations générées :

Pour (Direction direction : Direction._values()) caractère.try_moving_in_direction(direction);
Vous pouvez générer des énumérations avec des plages clairsemées, puis compter :

BETTER_ENUM(Drapeaux, char, Alloué = 1, InUtilisé = 2, Visité = 4, Inaccessible = 8) Flags::_size(); // 4
Si vous travaillez en C++11, vous pouvez même générer du code basé sur des énumérations car toutes les conversions et boucles peuvent être effectuées au moment de la compilation à l'aide des fonctions constexpr. Vous pouvez, par exemple, écrire une fonction constexpr qui calculera la valeur maximale d'une énumération et la rendra disponible au moment de la compilation. Même si les valeurs des constantes sont choisies arbitrairement et ne sont pas déclarées par ordre croissant.

Vous pouvez télécharger un exemple d'implémentation de macro depuis Github, regroupé dans une bibliothèque appelée Better Enums. Il est distribué sous licence BSD, vous pouvez donc en faire ce que vous voulez. Cette implémentation a un fichier d'en-tête, elle est donc très simple à utiliser, il suffit d'ajouter enum.h au dossier du projet. Essayez-le, cela vous aidera peut-être à résoudre vos problèmes.

Comment cela marche-t-il

Pour effectuer des conversions entre valeurs de chaîne et valeurs d'énumération, il est nécessaire de générer le mappage approprié. Better Enums fait cela en créant deux tableaux au moment de la compilation. Par exemple, si vous avez une déclaration comme celle-ci :

BETTER_ENUM(Direction, int, Nord = 1, Sud = 2, Est = 4, Ouest = 8)
alors la macro le transformera en quelque chose comme ceci :

Struct Direction ( enum _Enum : int (Nord = 1, Sud = 2, Est = 4, Ouest = 8); static const int _values ​​​​= (1, 2, 4, 8); static const char * const _names = ( "Nord", "Sud", "Est", "Ouest"); int _value; // ...Fonctions qui utilisent la déclaration ci-dessus... );
Et puis il procédera à la transformation : il trouvera l'index de la valeur ou de la chaîne dans _values ​​​​ou _names et renverra sa valeur ou chaîne correspondante dans un autre tableau.

Tableau de valeurs

_values ​​​​est généré en accédant aux constantes de l'énumération interne _Enum . Cette partie de la macro ressemble à ceci :

Const statique int _values ​​​​= (__VA_ARGS__);
Il se transforme en :

Static const int _values ​​​​= (Nord = 1, Sud = 2, Est = 4, Ouest = 8) ;
Ce presque déclaration de tableau correcte. Le problème vient des initialiseurs supplémentaires comme "=1". Pour travailler avec eux, Better Enums définit un type d'assistance conçu pour l'opérateur d'affectation, mais ignore la valeur attribuée elle-même :

Modèle struct _eat ( T _value; modèle _eat& Operator =(Toute valeur) ( ​​​​return *this; ) // Ignore l'argument.
explicite _eat(T value) : _value(value) ( ​​​​) // Convertit à partir de T. Operator T() const ( return _value; ) // Convertit en T. )

Vous pouvez désormais inclure l'initialiseur "=1" dans une expression d'affectation qui n'a aucune valeur :<_Enum>Const statique int _values ​​​​= ((_eat<_Enum>)Nord = 1, (_manger<_Enum>)Sud = 2, (_manger<_Enum>)Est = 4, (_manger

)Ouest = 8);

Tableau de chaînes

Pour créer ce tableau, Better Enums utilise (#), un opérateur de stringisation de préprocesseur. Il convertit __VA_ARGS__ en quelque chose comme ceci :
Char const statique * const _names = ("Nord = 1", "Sud = 2", "Est = 4", "Ouest = 8"); presque Maintenant nous

Noms de constantes convertis en chaînes. Reste à se débarrasser des initialiseurs. Cependant, Better Enums ne fait pas cela. C'est juste que lors de la comparaison des chaînes dans le tableau _names, il traite les espaces et les caractères égaux comme des limites de chaîne supplémentaires. Ainsi, lorsque vous recherchez « Nord = 1 », Better Enums ne trouvera que « Nord ».

Est-il possible de se passer de macro ?

À peine. Le fait est qu'en C++, l'opérateur (#) est le seul moyen de convertir un jeton de code source en chaîne. Ainsi, toute bibliothèque qui convertit automatiquement les énumérations avec réflexion doit utiliser au moins une macro de haut niveau.

Autres considérations

Bien entendu, envisager pleinement l’implémentation d’une macro serait bien plus ennuyeux et compliqué que ce qui est fait dans cet article. Fondamentalement, des difficultés surviennent en raison de la prise en charge des fonctions constexpr fonctionnant avec des tableaux statiques, en raison des caractéristiques des différents compilateurs. De plus, certaines difficultés peuvent être associées à la décomposition d'une grande partie de la macro en modèles afin d'accélérer la compilation (les modèles n'ont pas besoin d'être réanalysés lors de la création, mais les extensions de macro le font). Balises :

Énumération C.

Type d'énumération

En C, un type d'énumération distinct est alloué, qui spécifie l'ensemble de toutes les valeurs entières possibles d'une variable de ce type. Syntaxe d'énumération<имя> { <имя поля 1>, <имя поля 2>, ... <имя поля N>Énumération

); // se tient ici ;!

Par exemple #inclure #inclure

enum Sexe (MÂLE, FEMME); void main() ( enum Sexe a, b; a = MÂLE; b = FEMALE; printf("a = %d\n", a); printf("b = %d\n", b); getch() ; )

Ce programme déclare une énumération nommée Gender. Une variable de type enum Gender ne peut désormais prendre que deux valeurs - MALE AND FEMALE.

Par exemple #inclure enum Token ( SYMBOLE, //0 NUMÉRO, //1 EXPRESSION = 0, //0 OPERATEUR, //1 NON DÉFINI //2 ); void main() ( enum Token a, b, c, d, e; a = SYMBOLE; b = NUMÉRO; c = EXPRESSION; d = OPERATEUR; e = NON DÉFINI; printf("a = %d\n", a) ; printf("b = %d\n", b); printf("c = %d\n", c); getch( );

La sortie sera 0 1 0 1 2. Autrement dit, SYMBOLE est égal à EXPRESSION et NUMBER est égal à OPERATEUR. Si nous changeons le programme et écrivons

Jeton Enum ( SYMBOLE, //0 NUMÉRO, //1 EXPRESSION = 10, //10 OPERATEUR, //11 NON DÉFINI //12 );

Alors SYMBOLE sera égal à la valeur 0, NUMÉRO sera égal à 1, EXPRESSION sera égal à 10, OPERATEUR sera égal à 11, NON DÉFINI sera égal à 12.

Il est d'usage d'écrire les noms des champs d'énumération, comme les constantes, en majuscules. Étant donné que les champs d'énumération sont de type entier, ils peuvent être utilisés dans une instruction switch.

Notez que nous ne pouvons pas attribuer une variable Token uniquement à une valeur numérique. La variable est une entité de type Token et accepte uniquement les valeurs des champs d'énumération. Cependant, une variable numérique peut se voir attribuer la valeur d'un champ d'énumération.

Les énumérations sont généralement utilisées comme un ensemble de constantes nommées. Ils effectuent souvent ce qui suit : créer un tableau de chaînes associées aux champs d'énumération. Par exemple

Par exemple #inclure #inclure char statique *ErrorNames = ("Index hors limites", "Débordement de pile", "Sous-dépassement de pile", "Mémoire insuffisante" ); enum Erreurs ( INDEX_OUT_OF_BOUNDS = 1, STACK_OVERFLOW, STACK_UNDERFLOW, OUT_OF_MEMORY ); void main() ( //une erreur s'est produite printf(ErrorNames); exit(INDEX_OUT_OF_BOUNDS); )

Puisque les champs prennent des valeurs numériques, ils peuvent être utilisés comme index dans un tableau de chaînes. La commande exit(N) doit recevoir un code d'erreur autre que zéro car 0 est une sortie planifiée sans erreur. C'est pourquoi le premier champ d'énumération est égal à un.

Les énumérations sont utilisées pour une plus grande sécurité de type et pour limiter les valeurs possibles d'une variable. Afin de ne pas écrire une énumération à chaque fois, vous pouvez déclarer un nouveau type. Cela se fait de la même manière que dans le cas des structures.

Typedef enum enumName ( FIELD1, FIELD2 ) Nom ;

Comme on le sait, transferts- c'est un type qui peut contenir des valeurs spécifiées par le programmeur. Les constantes nommées entières peuvent être définies comme membres d’une énumération. Par exemple:

Enum (ROUGE, VERT, BLEU); définit trois constantes entières et leur attribue des valeurs. Par défaut, les valeurs sont attribuées dans l'ordre en partant de zéro, c'est-à-dire ROUGE == 0 , VERT == 1 et BLEU == 2 . Une énumération peut également être nommée : enum couleur (ROUGE, VERT, BLEU) ; Chaque énumération est un type distinct et le type de chaque membre de l'énumération est l'énumération elle-même. Par exemple RED est de type color . Déclarer le type d'une variable comme color , au lieu de l'habituel unsigned , peut indiquer à la fois au programmeur et au compilateur comment la variable doit être utilisée. Par exemple : void f(color c) ( switch(c)( case RED: // fait quelque chose de cassé; case BLUE: // fait quelque chose de break; ) )

Dans ce cas, le compilateur peut émettre un avertissement indiquant que seules deux des trois valeurs de couleur possibles sont traitées.

Les énumérations sont donc :

Création de constantes nommées avec incrémentation automatique de la valeur constante

Avertissements sur d'éventuelles erreurs du compilateur

Principaux problèmes lors de l'utilisation d'enum

En fait, tout ce qui précède est constitué de mots généraux qui ne sont nécessaires que pour que ceux qui ont erré ici par erreur retiennent au moins quelque chose de cet article. Et maintenant, nous allons parler des difficultés et des astuces auxquelles doivent faire face tous ceux qui utilisent plus ou moins les transferts dans le développement normal. Alors, à quoi devez-vous faire face :

1. Mapper la valeur de l'énumération sur une chaîne qui correspond au nom du membre de l'énumération, c'est-à-dire tout ce qui pour enum_map renverra "RED" .

2. Itération sur les membres du dénombrement et contrôle du dépassement des frontières. Ceux. Quel que soit le nombre de nouveaux éléments que vous ajoutez à l’énumération, vous disposez toujours d’une constante qui est exactement un de plus que le dernier membre de la séquence.

3. (Ceux qui n'ont pas réussi le test du modèle n'ont pas besoin de le lire) Mappage d'une variable entière d'exécution dans une variable ou un type de compilation (pointeur vers une fonction avec une valeur spécifique pour un paramètre de modèle, etc.).

Examinons les tâches ci-dessus en détail.

Mappage des membres de l'énumération aux chaînes

Mauvaise solution : const char* const strs=("ROUGE","GREEN","BLEU"); void f(couleur c) ( puts(strs[c]); )

Il ne peut y avoir que deux justifications à l’utilisation d’un tel code :

  • S'il existe une vérification statique de l'égalité du nombre de membres de l'énumération et du nombre d'éléments dans le tableau de chaînes (vous pouvez en savoir plus sur les vérifications statiques sur Alexandrescu ou attendre mon prochain article, qui sera écrit sur ce sujet). L'excuse est assez faible.
  • Si les chaînes peuvent ne pas coïncider avec les noms des membres de l’énumération (un cas rare en fait). Ceux. quelque chose comme ceci : const char* const strs=("Rouge comme une rose d'Egypte", "Paix verte", "Écran bleu de la mort"); .
Solution:

Éloignez vos enfants des écrans !
il y a des scènes à caractère immoral et pornographique

Transformons cette élégante énumération que nous avions :

Couleur d'énumération (ROUGE, VERT, BLEU) ;

ici dans ce monstre de Frankenstein :

couleur.h

#include "enum_helper_pre.h" enumeration_begin (color) declare_member (RED) délimiteur declare_member (GREEN) délimiteur declare_member (BLUE) enumeration_end ; #include "enum_helper_post.h"

Cela semble effrayant, mais en réalité, nous avons simplement ajouté la possibilité de remplacer chaque élément de la construction enum :

enum_helper_pre.h

#ifndef delimiter #define délimiteur , #endif #ifndef enumeration_begin #define enumeration_begin (arg) enum arg ( #endif #ifndef enumeration_end #ifdef last_enumerator #define enumeration_end delimiter last_enumerator ) #else #define enumeration_end ) #endif #endif #ifndef declare_member #define declare_member (arg) arg #endif #ifndef member_value #define member_value (arg) = arg #endif Je pense qu'il est clair qu'à la fin pour toutes ces macros, vous devez faire #undef :

enum_helper_post.h

#undef délimiteur #undef enumeration_begin #undef enumeration_end #undef last_enumerator #undef declare_member #undef member_value

Maintenant, si vous devez remplir un tableau avec des représentations sous forme de chaîne des membres de l'énumération, cela ressemblera à ceci :

main.c

Par exemple #define enumeration_begin(arg) const char* const arg##_strs=( #define declare_member(arg) #arg #include "color.h" #include "color.h" int main(int argc,char* argv) ( unsigned c = ROUGE; tandis que(c<= BLUE) { puts(color_strs); } return 0; }

Sortir

ROUGE VERT BLEU

Voilà ! Le voici - trois lignes de code et pour toute énumération conçue selon nos règles, vous recevez un tableau de représentations sous forme de chaîne des membres de l'énumération, quel que soit le nombre de membres de l'énumération. Toute personne qui déboguera le code à l'aide de traces, dans lesquelles au lieu de nombres sans visage, il y aura des noms d'éléments, vous remerciera beaucoup.

Itérer sur les membres de l'énumération et contrôler les limites

Un œil expérimenté remarquera que l'exemple ci-dessus est un peu boiteux, car les énumérations dans les grands projets ont tendance à augmenter avec le temps et la limite supérieure du cycle doit être redéfinie à tout moment. Par exemple, si notre énumération définit des composants de couleur, alors nous pouvons y ajouter un composant qui détermine la luminosité de la couleur :

Enum couleur (ROUGE, VERT, BLEU, LUMINOSITÉ);

Maintenant, dans l'exemple, vous devrez changer la condition d'exécution de la boucle en while(c<= BRIGHTNESS) . Если такой цикл один на всю программу, то в этом нет ничего страшного, но если подобный цикл встречается в десятке мест, то, рано или поздно, вы начнёте забывать все места где надо поменять предельное значение цикла, что приведёт к трудноуловимым ошибкам.

Un autre exemple où de tels contrôles pourraient devenir notre cauchemar est la validation des données reçues d'une certaine source. Par exemple, un serveur tiers peut vous envoyer les valeurs d'une énumération via un socket, et pour chaque valeur possible, vous effectuez différentes actions. Imaginez si l'administration du serveur ajoutait un nouvel élément à la fin de l'énumération ou si des déchets arrivaient sur le socket. Ensuite, vous ne le remarquerez que lorsque votre client, écoutant le socket, envoie des données plus loin et que l'un des modules, ayant reçu ces données, plante. La solution dans ce cas serait un contrôle comme celui-ci :

Si(c< RED || c >LUMINOSITÉ) throw std::runtime_error("Mauvaise couleur reçue");

Et encore une fois, si vous ajoutez un nouvel élément à l'énumération, vous devrez apporter des modifications à tous les fichiers où de telles vérifications existent. Il existe une solution plus simple : ajouter un membre d'énumération factice avec un nom fixe, qui est toujours le dernier membre de l'énumération. Avec notre définition du fichier color.h, c'est aussi simple que d'éplucher des poires :

main.c

Par exemple #define enumeration_begin(arg) const char* const arg##_strs=( #define declare_member(arg) #arg #include "color.h" #define last_enumerator COLOR_END #include "color.h" int main(int argc,char* argv) ( non signé c = ROUGE; tandis que(c< COLOR_END) { puts(color_strs); } return 0; }

Sortir

ROUGE VERT BLEU LUMINOSITÉ

Vous pouvez désormais ajouter autant de membres que vous le souhaitez à l'énumération sans craindre de perdre quoi que ce soit (le compilateur vous avertira des constructions de commutateur inchangées).

Mappage du moment de l'exécution au moment de la compilation

Ai-je déjà mentionné qu'il y aura des modèles ?

Imaginez maintenant que vous disposez d'une famille de fonctions de modèle telles que template void f() , et, disons, encore une fois, un nombre vous parvient via le socket, et pour ce nombre (interprété comme couleur), vous devez appeler la fonction correspondante f . Une solution simple s'impose :

Vide f(couleur c) (switch(c) (cas ROUGE : f (); casser; cas VERT:f (); casser; cas BLEU:f

();

casser; cas LUMINOSITÉ :f< RED || c >(); casser; ) )

Beaucoup plus simple, n'est-ce pas ?

Exemple promis

Compliquons la tâche. Maintenant, nous devons non seulement mapper le membre de l’énumération à une chaîne, mais aussi vice versa. Et en même temps RED == -2 et BLUE == 5 . À l’aide d’énumérations standard, vous ne pouvez obtenir des résultats qu’en saisissant directement les données dans une carte cartographique.

La solution est en fait très simple, comme tout dans cet article :

couleur.h

#include "enum_helper_pre.h" enumeration_begin(color) declare_member(RED) member_value(-2) delimiter declare_member(GREEN) delimiter declare_member(BLUE) member_value(5) delimiter declare_member(BRIGHTNESS) enumeration_end; #include "enum_helper_post.h"

main.cpp

Par exemple #inclure #inclure #inclure #include "color.h" int main(int argc,char* argv) ( typedef boost::bimap type_carte ;<

Sortir

map_type color_map;

#define declare_member(arg) color_map.insert(map_type::value_type(arg,BOOST_PP_STRINGIZE(arg))) #define délimiteur ;

#define enumeration_begin(arg) #define enumeration_end #define member_value(arg) #include "color.h" std::cout

ROUGE BLEU -1 6

En fait, cela s'est avéré simple, mais de bon goût. De cette façon, en vous en tenant à un style légèrement lourd de description des énumérations, vous pouvez obtenir une flexibilité incroyable dans la façon dont vous les utilisez.

C'est en fait tout ce que je voulais vous dire ici ; si quelqu'un a des questions, je serai heureux d'y répondre. Je serais heureux si cet article facilite la vie difficile d'un programmeur pour quelqu'un.

Dernière mise à jour : 10/03/2018

En plus des types de données primitifs, C# a un type tel qu'énumération ou énumération. Les énumérations représentent un ensemble de constantes logiquement liées. Une énumération est déclarée à l'aide de l'opérateur enum. Vient ensuite le nom de l'énumération, après quoi le type de l'énumération est indiqué - il doit nécessairement représenter un type entier (byte, int, short, long). Si le type n'est pas explicitement spécifié, le type par défaut est int. Vient ensuite une liste d’éléments d’énumération séparés par des virgules :

Enum Jours ( Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche ) enum Heure : octet ( Matin, Après-midi, Soir, Nuit)

Opération d'énumération (Ajouter = 2, Soustraire = 4, Multiplier = 8, Diviser = 16)

Dans ce cas, les constantes d'énumération peuvent avoir les mêmes valeurs, ou vous pouvez même attribuer à une constante la valeur d'une autre constante :

Enum Color ( Blanc = 1, Noir = 2, Vert = 2, Bleu = Blanc // Bleu = 1 )

Chaque énumération définit en fait un nouveau type de données. Ensuite dans le programme on peut définir une variable de ce type et l'utiliser :

Enum Operation ( Add = 1, Subtract, Multiply, Divide ) class Program ( static void Main(string args) ( Operation op; op = Operation.Add; Console.WriteLine(op); // Add Console.ReadLine(); ) )

Dans le programme, nous pouvons attribuer une valeur à cette variable. Dans ce cas, sa valeur doit être une des constantes définies dans cette énumération. Autrement dit, bien que chaque constante corresponde à un nombre spécifique, nous ne pouvons pas lui attribuer de valeur numérique, par exemple, Operation op = 1 ; . Et aussi, si nous imprimons la valeur de cette variable sur la console, nous obtiendrons des constantes pour elles, pas une valeur numérique. Si vous avez besoin d'obtenir une valeur numérique, vous devez convertir en un type numérique :

Opération opérationnelle ; op = Opération.Multiplier ; Console.WriteLine((int)op); // 3

Il convient également de noter que l'énumération ne doit pas nécessairement être définie à l'intérieur de la classe ; elle peut être définie en dehors de la classe, mais dans l'espace de noms :

Enum Operation ( Add = 1, Subtract, Multiply, Divide ) class Program ( static void Main(string args) ( Operation op; op = Operation.Add; Console.WriteLine(op); // Add Console.ReadLine(); ) )

Souvent, une variable d'énumération agit comme une réserve d'état, en fonction de laquelle certaines actions sont effectuées. Examinons donc l'utilisation de l'énumération à l'aide d'un exemple plus réel :

Programme de classe ( opération enum ( Ajouter = 1, Soustraire, Multiplier, Diviser ) static void MathOp (double x, double y, Opération op) ( double résultat = 0,0 ; switch (op) ( case Operation.Add : résultat = x + y ; break; case Operation.Subtract: break; case Operation.Multiply: break; case Operation.Divide: break; result); ) static void Main(string args) ( // Nous définissons l'opération tapez en utilisant la constante Operation.Add, qui est égale à 1 MathOp(10, 5, Operation.Add); // Nous définissons le type d'opération en utilisant la constante Operation.Multiply, qui est égale à 3 MathOp(11, 5, Opération.Multiply());

Nous avons ici une énumération d’opérations qui représente les opérations arithmétiques. Nous avons également défini une méthode MathOp, qui prend deux nombres et un type d'opération comme paramètres. Dans la méthode Main, nous appelons la procédure MathOp deux fois, en lui transmettant deux nombres et le type d'opération.



Des questions ?

Signaler une faute de frappe

Texte qui sera envoyé à nos rédacteurs :