Comprendre les enum

Ce texte résulte d'un échange avec Yannick Triqueneaux, cohorte 08 du DDJV de l'Université de Sherbrooke.

Pour qui vient d'un autre langage que C++, par exemple Java ou C#, utiliser des enum en C++ peut surprendre à la fois de par les similitudes et les différences avec les pratiques dans ces autres langages. Ce qui suit est un très petit programme (expliqué de façion succincte au fur et à mesure) manipulant un type Jour défini sous la forme d'un enum global; l'idée ici est de vous dépanner, pas de couvrir toutes les possibilités d'un tel type.

Le code présenté ici est du code C++ 03. Avec C++ 11, des raffinements importants sont rendus possibles avec les enum, alors si votre compilateur supporte cette partie du nouveau standard, profitez-en!

Les bases

De prime abord, un enum en C++ est un type, nommé ou non, qui regroupe des constantes entières connues dès la compilation. À moins que le programme ne dicte explicitement des valeurs pour ces constantes, la valeur de chacune pour un même enum sera distincte de celles des autres (la première vaudra 0, la deuxième vaudra 1, la troisième vaudra 2, etc.). À moins que le programme n'impose une valeur explicitement à une constante énumérée, celle-ci vaudra un de plus que celle qui la précède dans l'ordre de leurs déclarations.

Ainsi, dans le programme suivant, A vaut 0, B vaut 1, C vaut -2, D vaut -1 et E vaut 0 (il est donc possible pour deux symboles d'une même énumération d'avoir la même valeur) :

enum { A, B, C = -2, D, E };

Une constante énumérée est un entier connu à la compilation. À ce titre, elle peut par exemple être utilisée pour déterminer le nombre d'éléments d'un tableau automatique :

enum { N = 10 };
float tab[N] { 0.0f }; // tableau de N float initialisés à zéro

Dans une classe, l'utilisation d'un enum peut surpendre.

Dans l'exemple de la classe Etat, à droite, un type énuméré Etat::Possibilite est défini comme pouvant prendre les valeurs Etat::AVANT, Etat::PENDANT et Etat::APRES.

Remarquez que c'est le nom de la classe qui qualifie les noms des symboles, en non pas le nom du type énuméré lui-même. Le programme de test (à droite lui aussi) met ceci en relief, que ce soit pas l'initialisation de l'instance etat du type Etat ou de la variable poss du type Etat::Possibilite.

À l'intérieur d'une méthode de la classe Etat, il n'est pas nécessaire de qualifier les symboles énumérés locaux, ces noms étant qualifiés par le nom de la classe (préfixe Etat::) ce qui est alors implicitement le cas. La définition de la méthode Etat::est_avant() en fait la démonstration de par la manière dont elle accède à AVANT.

class Etat {
public:
   enum Possibilite {
      AVANT, PENDANT, APRES
   };
private:
   Possibilite valeur_;
public:
   Etat(Possibilite poss) : valeur_{ poss }  {
   }
   bool est_avant() const noexcept {
      return valeur_ == AVANT;
   }
   // ...
};
int main() {
  Etat etat = Etat::PENDANT;
  Etat::Possibilite poss = Etat::APRES;
}

Déclaration du type Jour et de ses opérations clés – Jour.h

L'exemple à droite présente le type Jour, une énumération représentant les jours de la semaine. Ce type est global, pour les besoins de l'exemple.

Vous remarquerez la classe JourInvalide, représentant (sans grande surprise) un jour invalide, et qui servira à fins de levée d'exceptions. Pour les actions préventives, est_valide(j) sera vrai seulement si j est un Jour valide (entre Dimanche et Samedi inclusivement avec notre implémentation).

Qu'un Jour puisse être invalide peut surprendre : comment un Jour peut-il être invalide s'il est représenté par un élément d'un ensemble fini de constantes énumérées? En fait, un enum en C++ est un entier, et les compilateurs ne valident typiquement pas les bornes de validité des entiers utilisés pour les initialiser (simple question de vitesse d'exécution). Pour cette raison, nous devrons déployer quelques efforts en ce sens dans notre implémentation.

Les autres services vont de soi :

  • Le prédicat est_week_end(j) sera vrai seulement si j représente un Samedi ou un Dimanche
  • Les opérateurs de projection sur un flux et d'extraction d'un flux font le travail attendu et utilisent tous deux une représentation basée sur les noms (français) des jours (p. ex. : le texte "Lundi"sv, pas la constante Lundi) pour les interaction avec les flux, et
  • La fonction nom(j) retourne le nom du jour j
#ifndef JOUR_H
#define JOUR_H
#include <string_view>
#include <iosfwd>
enum Jour {
    Dimanche, Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi
};
class JourInvalide {};
bool est_valide(Jour);
bool est_week_end(Jour);
std::string_view nom(Jour);
std::istream& operator>>(std::istream&, Jour&);
std::ostream& operator<<(std::ostream&, Jour);
#endif

Définition des opérations sur un Jour Jour.cpp

L'implémentation des services associés au type Jour va comme suit :

  • Le prédicat est_valide(j) traite j comme un entier situé inclusivement entre deux bornes
  • Le prédicat est_week_end(j) traite aussi j comme un entier (bien que ce soit moins évident ici) et compare des instances de Jour à l'aide de l'opérateur ==
  • La fonction nom(j), pour être efficace, considère explicitement j comme un entier et s'en sert pour accéder à l'élément approprié d'un tableau global ::NOMS. Puisqu'il est possible de passer un j qui ne soit pas un Jour valide à cette fonction, une validation en est faite avant de l'utiliser. Ceci permet d'éviter les débordements de tableau et les bogues de sécurité qui en résulteraient)
  • Remarquez que ::NOMS est placé dans un espace nommé anonyme, ce qui le cache des yeux de l'éditeur de liens
  • L'extraction d'un Jour à partir d'un flux demande de consommer un nom potentiel de Jour, de vérifier sa présence dans ::NOMS et de construire le Jour approprié
  • La projection d'un Jour sur un flux est banale

Le passage de Jour à string est automatique en C# ou en Java, mais ne l'est pas en C++, où enum n'est qu'un int. Ceci explique que le passage de l'un à l'autre doive être implémenté manuellement ici. Une implémentation complète demanderait que l'on tienne compte de l'internationalisation, ce que nous ne ferons pas ici, faute d'espace et de temps.

#include "Jour.h"
#include <string>
#include <istream>
#include <ostream>
#include <algorithm>
using namespace std;
bool est_valide(Jour j) {
   return Dimanche <= j && j <= Samedi;
}
bool est_week_end(Jour j) {
   return j == Samedi || j == Dimanche;
}
namespace {
    const string_view NOMS[]  {
       "Dimanche"sv,
       "Lundi"sv, "Mardi"sv, "Mercredi"sv, "Jeudi"sv, "Vendredi"sv,
       "Samedi"sv
    };
}
string_view nom(Jour j) {
    if (!est_valide(j))
       throw JourInvalide{};
    return ::NOMS[static_cast<int>(j)];
}
istream& operator>>(istream &is, Jour &j) {
    if (!is) return is;
    string s;
    if (!(is >> s)) return is;
    if (auto p = find(begin(::NOMS), end(::NOMS), s); p == end(::NOMS))
       is.setstate(std::ios::failbit);
    else
       j = static_cast<Jour>(distance(begin(::NOMS), p));
    return is;
}
ostream& operator<<(ostream &os, Jour j) {
   return os << nom(j);
}

Programme de test

Un programme de test très simple est proposé à droite. Comme vous pouvez le constater, une fois quelques services de base implémentés (en particulier, ceux rélisant les entrées/ sorties sur des flux), utiliser un type énuméré est simple et, faut bien le dire, plutôt naturel.

#include "Jour.h"
#include <iostream>
int main() {
   using namespace std;
   cout << "Entrez un jour: ";
   if (Jour j; cin >> j)
      if (est_week_end(j))
          cout << j << " est un jour de fin de semaine!" << endl;
      else {
          cout << j << " est un jour de semaine!" << endl;
          if (j == LUNDI)
             cout << "Oh non, pas un lundi!?!?!" << endl;
      }
}

Combiner énumérations et traits

Ce qui suit découle d'un échange avec Kenzo Lespagnol, étudiant de la cohorte 07 du DDJV.

Peut-on utiliser des énumérations à titre de paramètres pour des templates? Il se trouve que oui. Ce qui suit est un petit exemple tout simple d'une telle combinaison.

Soit les types énumérés voyelles et jours proposés à droite, de même que des opérateurs de projection sur un flux pour chacun d'eux.

Notez que le nom est voyelles est... discutable, du moins pour des symboles qui sont essentiellement globaux

#include <iosfwd>
#include <string_view>
using namespace std::literals;
enum voyelles {
   a, e, i, o, u, y
};
enum jours {
   lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche
};
std::ostream& operator<<(std::ostream&, voyelles);
std::ostream& operator<<(std::ostream&, jours);

Soit le type générique utiliser<T>, qui offre des services pour utiliser (de manière très banale) un T.

template <class>
    struct utiliser;

Chacun des utiliser<T> offret trois services :

  • La méthode nom() qui retourne une chaîne de caractères descriptive du type
  • La méthode premier() qui retourne la première valeur acceptable du type, et
  • La méthode dernier() qui retourne la dernière valeur acceptable du type
template <>
   struct utiliser<voyelles> {
      using value_type = voyelles;
      static constexpr auto nom() {
         return "voyelles"sv;
      }
      static constexpr value_type premier() {
         return a;
      }
      static constexpr value_type dernier() {
         return y;
      }
   };
template <>
   struct utiliser<jours> {
      using value_type = jours;
      static constexpr auto nom() {
         return "jours"sv;
      }
      static constexpr value_type premier() {
         return lundi;
      }
      static constexpr value_type dernier() {
         return dimanche;
      }
   };

La fonction afficher<T>(ostream&) utiliser utiliser<T> pour décrire le type T sur le flux passé en paramètre.

template <class T>
   void afficher(std::ostream &os) {
      using std::endl;
      os << utiliser<T>::nom() << " : "
         << utiliser<T>::premier() << " .. "
         << utiliser<T>::dernier() << endl;
   }


Enfin, le programme principal montre comment le tout fonctionne. C'est tout simple.

#include <iostream>
#include <string_view>
using namespace std;
int main() {
   afficher<jours>(cout);
   afficher<voyelles>(cout);
}
ostream& operator<<(ostream &os, voyelles v) {
   static const auto noms = {
      'a', 'e', 'i', 'o', 'u', 'y'
   };
   return os << noms[v];
}
ostream& operator<<(ostream &os, jours j) {
   static const auto noms = {
      "lundi"sv, "mardi"sv, "mercredi"sv,
      "jeudi"sv, "vendredi"sv,
      "samedi"sv, "dimanche"sv
   };
   return os << noms[j];
}

Lectures complémentaires

Pour éviter de la redondance, j'ai localisé les liens complémentaires sur les énumérations à l'adresse ../Divers--cplusplus/enumerations_fortes.html#lectures_complementaires.


Valid XHTML 1.0 Transitional

CSS Valide !