Quand plusieurs états doivent être représentés dans un programme C++, une simple liste de constantes devient vite source d’ambiguïtés. L’enum class apporte une solution plus sûre grâce à un typage strict, une portée clairement délimitée et un meilleur contrôle des conversions. Je vous montre son fonctionnement, ses différences avec l’ancien enum, ses usages concrets et les pièges à éviter dans un projet moderne.
Les repères essentiels pour choisir une énumération sûre en C++
- Typage fort : les valeurs ne se convertissent pas implicitement en entiers.
- Portée maîtrisée : chaque valeur s’utilise avec le nom de son type.
- C++11 : cette fonctionnalité est disponible depuis cette version du langage.
-
Type sous-jacent :
intpar défaut, ou un type entier choisi explicitement. -
Conversion : utilisez un cast explicite ou
std::to_underlyingen C++23.

Pourquoi utiliser une énumération fortement typée
Une énumération représente un ensemble fini de valeurs possibles. Elle convient par exemple à un statut de commande, un niveau de journalisation ou un mode de connexion. Avec une forme fortement typée, je peux exprimer clairement qu’une valeur appartient à un domaine précis, sans la traiter par erreur comme un simple nombre.
enum class StatutCommande {
Brouillon,
Payee,
Expediee,
Annulee
};
Les valeurs doivent être qualifiées avec le nom de l’énumération, comme dans StatutCommande::Payee. Cette contrainte peut sembler plus verbeuse au début, mais elle évite les collisions entre noms et rend le code beaucoup plus lisible lorsque plusieurs types possèdent des valeurs similaires.
Le compilateur refuse aussi les comparaisons ou affectations douteuses. Une variable de type StatutCommande ne peut pas recevoir directement un entier et ne peut pas être comparée sans précaution à une valeur numérique. Pour moi, c’est l’un de ses principaux intérêts dans les applications métier, où une confusion entre deux états peut produire un comportement difficile à repérer.
Comment déclarer et organiser les valeurs
La syntaxe reste simple. Si aucune valeur n’est indiquée, le premier élément reçoit généralement la valeur 0, puis les suivants sont incrémentés. Je préfère toutefois attribuer explicitement les nombres lorsque l’énumération est enregistrée dans un fichier, envoyée sur un réseau ou utilisée dans une base de données.
enum class NiveauLog : unsigned char {
Info = 1,
Alerte = 2,
Erreur = 3,
Critique = 4
};
La mention : unsigned char fixe le type sous-jacent, c’est-à-dire le type entier utilisé en interne pour stocker la valeur. Sans indication, une énumération fortement typée utilise généralement int. Un type plus petit peut réduire la taille d’une structure, mais ce gain ne justifie pas toujours une optimisation prématurée.
Les valeurs peuvent aussi être espacées volontairement. C’est utile pour préserver la compatibilité avec un protocole existant ou réserver des codes futurs.
enum class TypeMessage : std::uint8_t {
Texte = 1,
Image = 10,
Audio = 20
};
Dans cet exemple, modifier l’ordre des lignes ne change pas les codes déjà définis. C’est une précaution importante pour les données persistantes. Une énumération destinée uniquement à un switch interne peut rester séquentielle, tandis qu’une valeur exposée à l’extérieur doit recevoir un identifiant stable.
Quelle différence avec un enum classique
Le choix entre les deux formes n’est pas seulement une question de style. L’ancien enum est plus permissif, alors que la version fortement typée impose des règles qui protègent le programme. Voici la différence la plus utile à retenir.
| Critère |
enum classique |
Énumération fortement typée |
|---|---|---|
| Accès aux valeurs | Payee |
StatutCommande::Payee |
| Conversion implicite vers un entier | Souvent autorisée | Refusée |
| Risque de collision de noms | Plus élevé | Très limité |
| Type sous-jacent par défaut | Dépend de l’implémentation | int |
| Compatibilité avec du code ancien | Excellente | Demande parfois des conversions explicites |
Le code ancien peut donc sembler plus pratique, notamment lors d’un affichage ou d’un appel à une API qui attend un entier. En pratique, cette souplesse transfère le risque vers le développeur. Je recommande généralement la forme fortement typée pour tout nouveau code, sauf lorsqu’une bibliothèque ou une interface historique impose un enum classique.
Il existe une exception utile en C++20 avec using enum, qui permet d’importer temporairement les valeurs dans une portée. Je l’utilise avec modération, car elle réduit une partie de la protection offerte par le nom qualifié et peut réintroduire des ambiguïtés dans une fonction volumineuse.
Comment convertir une valeur sans perdre la sécurité
Le refus des conversions implicites est une protection, pas une limitation définitive. Lorsqu’il faut afficher un code, remplir un champ binaire ou appeler une fonction ancienne, la conversion doit simplement être explicite.
StatutCommande statut = StatutCommande::Payee;
int code = static_cast(statut);
Le static_cast rend l’intention visible. Le lecteur voit immédiatement qu’une valeur symbolique est transformée en nombre, et le compilateur ne peut pas confondre cette opération avec une conversion accidentelle.
En C++23, std::to_underlying fournit une écriture plus claire pour récupérer le type sous-jacent.
#include
auto code = std::to_underlying(StatutCommande::Payee);
Pour revenir d’un entier vers l’énumération, je déconseille de convertir aveuglément une donnée externe. Il faut d’abord vérifier que le nombre correspond bien à une valeur autorisée.
int code = lire_code();
if (code == 1) {
statut = StatutCommande::Payee;
} else {
statut = StatutCommande::Annulee;
}
Cette validation devient indispensable avec les fichiers, les messages réseau et les entrées utilisateur. Une donnée externe peut contenir une valeur inconnue à la suite d’une évolution du logiciel. Prévoir un état de repli ou retourner une erreur est plus sûr que de supposer que tous les composants utilisent exactement la même version.
Les usages qui apportent le plus de valeur
Les états métier et les machines à états
Une énumération fortement typée convient très bien à une machine à états simple. Une commande peut passer de Brouillon à Payee, puis à Expediee, tandis qu’une transition interdite doit être rejetée. Le type rend les états explicites, mais la logique de transition doit rester dans une fonction dédiée, sinon le programme devient difficile à maintenir.
void traiter(StatutCommande statut) {
switch (statut) {
case StatutCommande::Brouillon:
break;
case StatutCommande::Payee:
break;
case StatutCommande::Expediee:
break;
case StatutCommande::Annulee:
break;
}
}
Lorsque tous les cas sont traités, le compilateur et les outils d’analyse peuvent mieux signaler l’ajout d’un nouvel état. Je conseille de compiler avec des avertissements élevés, car une nouvelle valeur oubliée dans un switch peut révéler un défaut fonctionnel.
Les options et les indicateurs binaires
Cette technique peut aussi représenter des drapeaux binaires, mais elle demande plus de travail. Les opérateurs |, & et ~ ne fonctionnent pas automatiquement comme avec un entier. Il faut définir des opérateurs adaptés ou utiliser une abstraction prévue pour les ensembles de bits.
enum class Permission : unsigned int {
Lecture = 1u << 0,
Ecriture = 1u << 1,
Administration = 1u << 2
};
Cette approche est pertinente pour des permissions ou des options indépendantes. Elle l’est moins pour des états mutuellement exclusifs, comme Connecte et Deconnecte. Mélanger les deux modèles rend le code ambigu et complique les tests.
Lire aussi : Classe Abstraite - Le guide complet pour maîtriser son usage
L’affichage et la conversion en texte
Une énumération ne connaît pas automatiquement le texte que vous souhaitez afficher. Transformer sa valeur en nombre ne donnera pas « Payée », mais son code interne. Une fonction de conversion explicite est donc préférable.
std::string_view vers_texte(StatutCommande statut) {
switch (statut) {
case StatutCommande::Brouillon: return "Brouillon";
case StatutCommande::Payee: return "Payée";
case StatutCommande::Expediee: return "Expédiée";
case StatutCommande::Annulee: return "Annulée";
}
return "Inconnu";
}
Le cas Inconnu joue un rôle utile lorsqu’une valeur provient d’une source non maîtrisée. Pour une application traduite, je séparerais le code métier du texte affiché et confierais la traduction à la couche d’interface.
Les erreurs qui reviennent le plus souvent
La première erreur consiste à croire que l’énumération garantit à elle seule une valeur valide. Elle protège le type, mais un cast explicite peut toujours introduire un nombre qui ne correspond à aucun élément déclaré. Les frontières entre le programme et l’extérieur doivent donc rester contrôlées.
La deuxième erreur est de modifier l’ordre des valeurs utilisées pour la sérialisation. Si Brouillon vaut aujourd’hui 0 et devient 1 après une réorganisation, les anciennes données peuvent être interprétées de travers. J’attribue des valeurs fixes dès qu’une donnée quitte la mémoire du processus.
Il faut aussi éviter de choisir un type sous-jacent minuscule uniquement pour économiser quelques octets. Cette décision peut compliquer les opérations, provoquer des conversions inattendues et limiter les valeurs disponibles. Le bon critère est la compatibilité avec le format de données, puis seulement l’optimisation de la mémoire.
Enfin, une énumération ne doit pas remplacer une classe lorsque le domaine a un comportement riche. Si chaque valeur possède des règles, des transitions, des données associées et plusieurs opérations, un type plus structuré sera souvent plus clair. L’énumération est excellente pour nommer un choix fini, pas pour porter toute la logique applicative.
Le bon réflexe avant de l’intégrer dans un projet
Pour un nouveau développement C++, je commence par vérifier si les valeurs sont mutuellement exclusives, si elles doivent être stockées ou transmises, et si une bibliothèque existante impose un format précis. Ces trois questions déterminent le choix du type sous-jacent, l’attribution des codes et la stratégie de validation.
Dans la majorité des cas, la forme fortement typée offre le meilleur équilibre entre lisibilité et sécurité. Elle demande quelques caractères supplémentaires, mais elle évite des conversions silencieuses qui coûtent bien plus cher à diagnostiquer lorsque le projet grandit.
Mon conseil est donc simple. Utilisez-la par défaut pour les nouveaux types d’énumération, donnez des valeurs explicites aux données persistantes, validez les nombres provenant de l’extérieur et réservez les conversions aux endroits où elles sont réellement nécessaires. Ce petit cadre suffit à rendre les états du programme plus prévisibles et les évolutions beaucoup moins risquées.