Loading [MathJax]/jax/output/HTML-CSS/jax.js

Introduction aux énumérations fortes

Ce court texte a été suggéré par Maxime Grégoire de Eidos/ Square Enix.

Ce qui suit offre un survol rapide des « énumérations fortes » de C++ 11. Cette particularité du nouveau standard est à la fois très simple et très utile – en fait, je pense qu'il est raisonnable d'affirmer que les « énumérations fortes » sont en tout point meilleures que les énumérations « classiques », qui prévalaient depuis les débuts du langage C.

Énumérations classiques

Supposons que nous souhaitions représenter une carte dont chaque case puisse être soit vide, soit occupée par un mur, un héros, une bestiole ou une crapule.

Une énumération classique pourrait permettre d'exprimer les valeurs possibles pour une case de la manière suivante :

enum ContenuCase { Vide, Mur, Heros, Bestiole, Crapule };

Un exemple d'utilisation serait :

#include "ContenuCase.h"
#include <algorithm>
#include <random>
#include <vector>
constexpr bool est_vilain(ContenuCase cc) {
   return cc == Bestiole || cc == Crapule;
}
int main() {
   using namespace std;
   enum {
      LARGEUR = 25'000,
      HAUTEUR = 25'000,
      PROB_VIDE = 85,
      NB_CASES = LARGEUR * HAUTEUR // 625'000'000
   };
   vector<ContenuCase> carte(NB_CASES, Vide);
   {
      uniform_int_distribution<> d4 { 1,4 };
      uniform_int_distribution<> d100 { 1,100 };
      random_device rd;
      mt19937 prng{ rd() };
      generate(begin(map), end(map), [&]() -> ContenuCase {
         return d100(prng) >= PROB_VIDE? static_cast<ContenuCase>(d4(prng)) : Vide;
      });
   }
   // ...utiliser carte...
}

Remarquez que les noms des valeurs énumérées sont ici des noms globaux (p. ex. : Crapule, pas ContenuCase::Crapule), ce qui est un irritant important pour ce qui est de la pollution de l'espace de noms anonyme, là où se logent les noms globaux. Par exemple, si nous avions une classe Vide, nous aurions ici un conflit de nom avec la valeur entière Vide de ContenuCase. Il arrive que ces noms sans qualifications soient convenables (pensons aux constantes locales LARGEUR et HAUTEUR dans main() par exemple), mais dans un cas comme celui des valeurs de ContenuCase, les risques sde conflit sont grands.

Notez que j'ai nommé ce type ContenuCase pour que l'exemple demeure simple, mais il aurait sans doute été préférable en pratique d'exprimer la chose comme suit :

class Case {
   // ...
public:
   enum Contenu {
      Vide, Mur, Heros, Bestiole, Crapule
   };
   // ...
};

Ceci aurait contraint les noms au contexte déclaratif de Case, un peu comme dans un espace nommé (Case::Mur ou Case::Vide, par exemple), ce qui règle dans bien des cas le problème de la pollution de l'espace nommé anonyme.

Autre irritant des énumérations classiques, dans un cas comme celui-ci : le substrat de représentation des valeurs est un int, même si dans un cas comme celui-ci il aurait été possible de se restreindre à un char tout en couvrant la totalité de la plage de valeurs possible. Conséquence : notre carte occupe au moins 625000000×sizeof(int) bytes en mémoire... Environ 2500000000 bytes, donc ≈2 Go si sizeof(int)==4. Si nous avions pu représenter les valeurs sur un char, donc aurions pu restreindre cette consommation d'espace à ≈500 Mo, une économie pour le moins substantielle.

Énumérations fortes

À partir de cet exemple, nous pouvons mettre en relief les avantages des énumérations fortes : elles placent les valeurs énumérées en contexte, et elles offrent un contrôle sur le substrat de représentation. Deux avantages simples mais très utiles et très pertinents.

Mise en contexte des valeurs

Pour obtenir une énumération forte, et par le fait-même des valeurs placées en contexte, il suffit d'ajouter un petit mot à la déclaration du type énuméré, soit le mot class (le mot struct peut aussi y être appliqué, avec le même effet, mais on le voit plus rarement en pratique) :

enum class ContenuCase { Vide, Mur, Heros, Bestiole, Crapule };

Par ce simple ajout, les valeurs de ContenuCase deviennent non pas Vide, Mur, Heros... mais bien ContenuCase::Vide, ContenuCase::Mur, ContenuCase::Heros... Outre pour ce qui est du support à du code vieillissant et des constantes locales simples (voir LARGEUR et HAUTEUR dans main(), plus haut), cette contextualisation des noms est presque toujours un avantage.

Choix du substrat de représentation des valeurs

Choisir le substrat de représentation des valeurs énumérées est aussi simple que de « dériver » l'énumération du substrat choisi (j'abuse bien sûr du mot « dériver » ici) :

// on peut aussi écrire « enum struct », et obtenir le même résultat
enum class ContenuCase : char { Vide, Mur, Heros, Bestiole, Crapule };

Par ce simple ajout, les valeurs de ContenuCase seront représentées par des valeurs sur un seul byte, une économie d'échelle importante.

En résumé

Reprenons l'exemple d'utilisation plus haut pour voir l'impact de cette nouvelle notation :

#include "ContenuCase.h"
#include <algorithm>
#include <random>
#include <vector>
constexpr bool est_vilain(ContenuCase cc) {
   return cc == ContenuCase::Bestiole || cc == ContenuCase::Crapule;
}
int main() {
   using namespace std;
   enum {
      LARGEUR = 25'000,
      HAUTEUR = 25'000,
      PROB_VIDE = 85,
      NB_CASES = LARGEUR * HAUTEUR // 625'000'000
   };
   vector<ContenuCase> carte(NB_CASES, ContenuCase::Vide);
   {
      uniform_int_distribution<> d4{ 1,4 };
      uniform_int_distribution<> d100{ 1,100 };
      random_device rd;
      mt19937 prng{ rd() };
      generate(begin(map), end(map), [&]() -> ContenuCase {
         return d100(prng) >= PROB_VIDE? static_cast<ContenuCase>(d4(prng)) : ContenuCase::Vide;
      });
   }
   // ...utiliser carte...
}

Les changements d'écriture sont mineurs, et servent surtout à clarifier le code et à réduire les risques de conflits de noms. La consommation de mémoire du programme est quant à elle réduite d'environ 1,5 Go. Pas mal pour un si petit changement.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !