Enum class en C++ pour un code plus sûr et lisible

Alfred Jacques .

28 octobre 2026

Code C++ montrant une classe enum et sa traduction en assembleur x86-64. La fonction `square` utilise un switch sur l'enum.

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 : int par défaut, ou un type entier choisi explicitement.
  • Conversion : utilisez un cast explicite ou std::to_underlying en C++23.

Exemple de code C++ montrant la différence entre les énumérations non scopées et les énumérations scopées (`enum class`).

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.

Questions fréquentes

enum class impose une portée qualifiée, comme StatutCommande::Payee, et refuse les conversions implicites vers les entiers. L’enum classique est plus permissif, mais augmente les risques de collisions de noms et de conversions accidentelles.
Le type sous-jacent est généralement int par défaut, mais il peut être fixé explicitement avec un type entier comme unsigned char ou std::uint8_t. Pour des données stockées ou transmises, attribuez aussi des valeurs fixes afin de préserver la compatibilité.
Utilisez un static_cast lorsque vous devez appeler une ancienne API, remplir un champ binaire ou afficher un code. En C++23, std::to_underlying offre une écriture plus claire. Dans l’autre sens, validez toujours l’entier avant de le convertir, surtout s’il vient d’un fichier, du réseau ou d’une entrée utilisateur.
Oui, elle peut représenter des permissions ou des options avec des valeurs comme 1u << 0 et 1u << 1. Les opérateurs |, & et ~ ne fonctionnent toutefois pas automatiquement : il faut définir des opérateurs adaptés ou utiliser une abstraction dédiée. Ce modèle ne convient pas aux états mutuellement exclusifs.
Évaluer l'article

Moyenne: 0.0 / 5 · 0 évaluations

Tags

sérialisation énumérations typage fort machines à états type sous-jacent
Autor Alfred Jacques
Alfred Jacques
Je m'appelle Alfred Jacques et j'ai accumulé 12 ans d'expérience dans le domaine des technologies, en particulier dans les secteurs du web, de l'intelligence artificielle, des réseaux et de la sécurité. Mon intérêt pour ces sujets a débuté dès mon adolescence, lorsque j'ai découvert les possibilités infinies qu'offrent les nouvelles technologies. J'aime explorer les enjeux complexes de ces domaines et partager des informations claires et accessibles pour aider mes lecteurs à mieux comprendre les défis et les évolutions technologiques. Dans mes écrits, je m'efforce de fournir des analyses précises et à jour, en vérifiant mes sources et en comparant différentes perspectives. Je m'engage à simplifier des concepts parfois difficiles afin de rendre l'information utile et compréhensible pour tous. Que ce soit en suivant les dernières tendances ou en organisant mes connaissances de manière claire, je souhaite que mes articles soient une ressource précieuse pour ceux qui s'intéressent à la technologie et à ses implications dans notre quotidien.
Commentaires (0)
Ajouter un commentaire