Rappel énumérations
Petit rappel rapide de ce qu'est une énumération : c'est un type qui a une liste de constantes comme valeurs possibles, ces constantes sont des entiers connus à la compilation. Les valeurs de ces constantes peuvent être spécifiées explicitement ou bien avoir une valeur par défaut. Quand la valeur par défaut est utilisée, si c'est la première constante, sa valeur sera de 0, sinon elle vaudra la valeur de la constante précédente incrémentée de 1.
enum Example
{
FIRST, // = 0
SECOND = 3,
THIRD // = 4
};
Scoped énumération
Une scoped énumération est très similaire à une énumération simple. Tout d'abord, pour les déclarer, il faut ajouter le mot-clef struct ou class, (selon votre préférence personnelle, il n'y a aucune différence entre ces deux mots-clefs dans ce contexte) après le mot-clef enum.
enum class EnumClass
{
ONE = 1,
TWO = 2
};
enum struct EnumStruct
{
ONE = 1,
TWO = 2
};
Ensuite, les scoped énumérations, comme leur nom l'indique, créent un scope comme les structures et les classes (ce qui explique les mots clefs struct ou class) , ce qui veut donc dire que toutes les constantes n'existent que dans ce scope.
Voici un exemple si on reprend les deux énumérations de l'exemple précédent :
void test()
{
EnumClass enum_class = EnumClass::ONE;
EnumStruct enum_struct = EnumStruct::TWO;
}
Dernièrement, les scoped énumérations sont typés plus fortement. Techniquement, cela signifie qu'il n'y a plus de conversion implicite d'une énumération vers un entier, la conversion reste possible mais elle doit être explicite.
enum class Animals
{
CAT = 1,
DOG = 2,
RABBIT = 3
};
void cast_enum_to_integer()
{
int a = Animals::CAT; // Error
int b = static_cast<int>(Animals::DOG); // Good
}
Concrètement, quels sont les avantages vis-à-vis à une énumération simple ?
Cela permet de ne plus être obligé de préfixer toutes les constantes de ses énumérations pour ne pas avoir de conflits entre les noms.
enum class NetworkError
{
UNKNOWN,
CONNECTION_LOST,
INVALID_PARAMETERS
};
enum class FileError
{
UNKNOWN,
INVALID_PARAMETERS,
INVALID_FILE
};
Dans l'exemple ci-dessus, même si les deux énumérations ont des constantes avec les mêmes noms, cela ne pose aucun problème; alors qu'avec des énumérations simples une erreur aurait été levée à la compilation.
De plus, l'absence de conversion implicite vers des entiers permet d'éviter des erreurs assez triviales mais difficiles à trouver, où l'on utiliserait une énumération à la place d'un entier.
Le type sous-jacent
Le type sous-jacent est le type entier dans lequel est stocké l'énumération.
Le type par défaut est défini différemment entre les énumérations simples et les scoped énumération :
- Enumération simple : quand rien n'est spécifié, il est stocké dans un type entier défini par le compilateur, qui peut stocker toutes les valeurs possibles de l'énumération, et qui n'est pas plus grand qu'un int sauf si au moins une constante est trop grande pour rentrer dans un int.
- Scoped énumération : le type sous-jacent est un int, si une constante ne rentre pas dans un int, votre compilateur vous l'indiquera avec un message d'erreur sympathique.
Pour le spécifier c'est très simple, il suffit de mettre après le nom de l'énumération ":" suivi du type comme ceci :
enum Little: int
{
LittleA,
LittleB
};
enum class ScopedLittle: char
{
A,
B
};
La seule contrainte est que le type doit être un type entier.
Connaitre le type sous-jacent
Si vous faites de la programmation générique, ou même si vous aimez les templates tout simplement, il se peut que vous soyez amenés à avoir besoin de connaître le type sous-jacent d'une énumération. Pour cela, il existe dans la bibliothèque standard la structure std::underlying_type.
Et voici une fonction qui permet de convertir automatiquement n'importe quelle énumération en son type sous-jacent en l'utilisant.
template <typename Enum>
constexpr typename std::underlying_type<Enum>::type underlying_type_cast(Enum e)
{
return static_cast<typename std::underlying_type<Enum>::type>(e);
}
Voici un exemple d'utilisation :
enum class Animals
{
CAT = 0,
DOG = 1,
RABBIT = 2
};
int main()
{
auto integer_cat = underlying_type_cast(Animals::CAT);
if (integer_cat == 0)
std::cout << "I love this cat !" << std::endl;
}
Forward declaration
En C++98 il n'était pas possible de faire une forward declaration d'une énumération car tant qu'elle n'est pas déclarée, le type sous-jacent n'est pas défini.
Avec C++11, il est possible de faire une forward déclaration d'une énumération si le type est défini, donc pour les unscoped énumérations dont le type est explicitement donné, ou pour les scoped énumérations :
enum A: int; // Ok
enum B; // Error
enum class C: char; // Ok
enum class D; // Ok
De plus, si le type sous-jacent diffère entre la forward declaration et la déclaration, une erreur sera levée lors de la compilation:
// Les types sous jacents sont les mêmes => ok
enum class C: char;
enum class C: char {};
// Les types sous-jacents sont différents => erreur
enum class D;
Faq
Quand utiliser quoi ?
Par défaut, il vaut mieux toujours utiliser une scoped énumération, elles sont plus sûres et il n'y a pas besoin d'utiliser des conventions de nommage arbitraires pour éviter les conflits de noms.
La seule exception qui me vient à l'esprit, c'est si le code doit être compatible C++98 ou avec du C, qui eux n'ont pas accès à cette fonctionnalité.
Quand spécifier le type sous-jacent ?
- Si vous avez besoin de faire une forward declaration de votre énumération et que vous utilisez une énumération simple.
- Si la taille du type sous-jacent est importante (cf. std::byte)
Top comments (8)
Tu sais s'il y a (eu) des discussions pour intégrer au langage la possibilité de mettre des fonctions membres à des
enum class
?Je n'en ai pas entendu parler, par contre on peut surcharger les opérateurs qui peuvent se déclarer en tant que fonction libres, comme par exemple les opérateurs de comparaisons, (==, !=, >, ...), les operateurs arithmétiques (+, -, ...) ou bien encore l'opérateurs ->*
Voici un exemple ici : godbolt.org/z/ebqzbWbMo
C'est déjà pas mal, mais on est encore loin d'une vraie classe, comme on peut l'avoir en Java. Peut-être en C++29...
J'avoue que je vois mal l'intérêt d'ajouter des méthodes à une énumération, le seul auquel je pense c'est pour faire des conversion en chaine de caractère par exemple et je préfère une simple fonction "to_string(Enum e)".
Si je veux que mes énumérateurs aient de la logique, j'utilise std::variant.
Sinon tu peux quand même tenter une enum dans une classe comme ça godbolt.org/z/5MWrb1We1 , l'opérateur d'assignement ou de comparaisons ne sont pas là par défaut donc faut un peu de code boiler plate, mais avec du CRTP ça devrait simplifier le processus.
C'est un contournement possible. Faudrait que j'essaye.
Effectivement, la nécessité de convertir depuis/vers des chaines de caractères est un besoin qui ressort vite.
J'ai aussi des codes où j'ai besoin de catégoriser les valeurs de l'énumération. Genre :
Ouai je vois du coup.
Vu que j'aime pinailler tu pourrais aussi faire comme ça :
En vrai tu perds l'auto-complétions ou bien c'est beaucoup plus verbeux. En plus le code est plus complexe, je pense pas que ça une meilleur solution.
L'exemple était un peu particulier et effectivement, une technique comme celle-ci serait plus intéressante.
Dans un code actuel, j'ai utilisé un namespace pour le nom de l'énumération, une enum class ID et des fonctions libres dans le namespace. Mais je ne suis pas satisfait à 100%. En reprenant l'exemple ci-dessus :
Ca reprends l'idée de la surcharge de l'opérateur ->* dont j'ai parlé précédemment, mais j'avais fait ça il y a quelque temps qui pourrait peut être faire ce que tu veux dans ton cas github.com/Baduit/Unic . Ca reprends l'idée de l'Uniform function call syntax qui existe dans d'autres langage (et il y a aussi des propositions d'intégration en C++ mais aucune n'a aboutit il me semble)