Utiliser enum dans les boucles et la cohérence des valeurs

Je suis un grand fan des fonctionnalités de typage fort du C ++ et ce que j’aime le plus, c’est d’utiliser des énumérations avec des ensembles de données limités.

Mais les énumérations manquent de fonctionnalités utiles, par exemple les opérateurs:

enum class Hex : int { n00, n01, n02, n03, n04, n05, n06, n07, n08, n09, n10, n11, n12, n13, n14, n15 }; for (Hex h = Hex::n0; h <= Hex::n15; ++h) // Oops! no 'operator ++' { /* ... */ } 

Il est facile de se débarrasser du manque d’opérateurs créant un opérateur libre sur le même périmètre:

 Hex &operator ++(Hex &h) { int r = static_cast(Hex); h = static_cast(r + 1); return h; } for (Hex h = Hex::n0; h <= Hex::n15; ++h) // Now the '++h' works! { std::cout << std::dec << int(h) << ": " << std::hex << int(h) << '\n'; } 

Mais cette approche est plus une gêne que une solution, car elle peut dépasser la limite de valeur de l’énumération: appliquer ++h pendant que h est égal à Hex::n15 fixera h à la valeur 16, ce qui sort du champ Hex valeurs alors que h est encore du type Hex !, Ce problème est plus évident dans les autres énumérations:

 enum class Prime : int { n00 = 2, n01 = 3, n02 = 5, n03 = 7, n04 = 11, n05 = 13, n06 = 17, n07 = 19, n08 = 23, n09 = 29, n10 = 31, n11 = 37, n12 = 41, n13 = 43, n14 = 47, n15 = 53 }; Prime &operator ++(Prime &p) { // How to implement this?! will I need a lookup table? return p; } 

Ce problème était une surprise pour moi; Je pariais que le stockage d’une valeur incorrecte dans une valeur d’énumération lève une exception. Donc, pour le moment, je me demandais s’il existait un moyen élégant de remédier aux faiblesses de ce recensement, les objectives que je souhaite atteindre sont les suivants:

  • Trouvez un moyen confortable d’utiliser les valeurs d’énumération dans les boucles.
  • Assurer la cohérence des données d’énumération entre les opérations.

Questions supplémentaires:

  • Existe-t-il une raison pour ne pas générer d’exception lorsqu’une donnée d’énumération obtient une valeur qui est hors de ses valeurs possibles?
  • Il existe un moyen de déduire le type associé à une classe d’énumération?, Le type int dans les énumérations Hex et Prime .

Comme vous l’avez remarqué, l’ enum en C ++ n’est pas un type énuméré, mais quelque chose de plus complexe (ou plus mélangé). Lorsque vous définissez une enum , vous définissez en fait deux choses:

  1. Un type intégral avec une plage légale suffisante pour contenir une ou toutes les valeurs énumérées. (Techniquement: la plage est 2^n - 1 , où n est le nombre de bits nécessaires pour conserver la valeur la plus grande.)

  2. Une série de constantes nommées ayant le type nouvellement défini.

(Je ne suis pas sûr de ce qui se passe concernant la plage si vous spécifiez explicitement un type sous-jacent.)

En fonction de votre enum Prime , par exemple, les valeurs légales seraient tous des entiers compris dans l’intervalle [0...64) , même si toutes ces valeurs n’ont pas de nom. (Du moins si vous n’avez pas dit spécifiquement que cela devrait être un int .)

Il est possible d’implémenter un iterator pour les énumérations sans initialiseurs; J’ai un programme qui génère le code nécessaire. Mais cela fonctionne en maintenant la valeur dans un type intégral suffisamment grand pour contenir la valeur maximale plus un. Ma machine générée implémentations de ++ sur une telle enum assert si vous essayez d’incrémenter au-delà de la fin. (Notez que votre premier exemple nécessiterait une itération supérieure à la dernière valeur: ma mise en œuvre des différents opérateurs ne le permet pas, raison pour laquelle j’utilise un iterator.)

Pour savoir pourquoi C ++ prend en charge la plage étendue: les enum sont souvent utilisés pour définir des masques de bits:

 enum X { a = 0x01, b = 0x02, c = 0x04, all = a | b | c, none = 0 }; X operator|( X lhs, X rhs ) { return X((int)lhs | (int)rhs); } // similarly, &, |= and &=, and maybe ~ 

On pourrait soutenir que cette utilisation serait mieux gérée par une classe, mais l’utilisation d’ enum est omniprésente.

(FWIW: mon générateur de code ne générera pas le ++ , et les iterators si l’une des valeurs enum a une valeur explicitement définie, et ne générera pas | , & etc. sauf si toutes les valeurs ont des valeurs explicitement définies.)

Quant à savoir pourquoi il n’ya pas d’erreur lorsque vous convertissez une valeur en dehors de la plage légale (par exemple, 100, pour X , ci-dessus), cela correspond tout simplement à la philosophie générale héritée de C: il vaut mieux être rapide que correct. Faire une vérification de plage supplémentaire impliquerait des coûts d’exécution supplémentaires.

Enfin, en ce qui concerne votre dernier exemple: je ne vois pas cela comme une utilisation réaliste d’ enum . La solution correcte ici est un int[] . Bien que l’ enum C ++ soit plutôt une race mélangée, je ne l’utiliserais que comme un type énuméré réel, ou pour les masques de bits (et uniquement pour les masques de bits, car c’est un idiome largement établi).

Vous pouvez utiliser un switch :

 class Invalid {}; Prime& operator ++(Prime& p) { switch(p) { case n00: return n01; case n01: return n02; case n02: return n03; case n03: return n04; case n04: return n05; case n05: return n06; case n06: return n07; case n07: return n08; case n08: return n09; case n09: return n10; case n10: return n11; case n11: return n12; case n12: return n13; case n13: return n14; case n14: return n15; // Here: 2 choices: loop or throw (which is the only way to signal an error here) case n15: default: throw Invalid(); } } 

Mais notez que ce n’est pas le bon usage des enums. Personnellement, je trouve cette source d’erreurs. Si vous souhaitez énumérer des entiers, vous pouvez utiliser un tableau d’entités pour le faire ou, dans le cas de nombres premiers, une fonction (au sens mathématique: int nextPrime(int) ).