DEV Community

Lena
Lena

Posted on

Enumérations, ce qui change avec C++11

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
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)

Sources:

Top comments (8)

Collapse
 
pgradot profile image
Pierre Gradot

Tu sais s'il y a (eu) des discussions pour intégrer au langage la possibilité de mettre des fonctions membres à des enum class ?

Collapse
 
baduit profile image
Lena

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

Collapse
 
pgradot profile image
Pierre Gradot

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...

Thread Thread
 
baduit profile image
Lena

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.

Thread Thread
 
pgradot profile image
Pierre Gradot

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 :

    enum Colors {
        GREEN_LIGHT, GREEN_DARK, RED_DARK

        bool isDark() const {
            return ....;
        }
    };

    auto c = Colors::GREEN_DARK;
    c.isDark();
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
baduit profile image
Lena • Edited

Ouai je vois du coup.
Vu que j'aime pinailler tu pourrais aussi faire comme ça :

struct Color
{
    enum class Bases
    {
        GREEN,
        RED
    };

    enum class Modifiers
    {
        LIGHT,
        DARK
    };

    Bases base;
    Modifiers modifier;

    bool isDark() const { return modifier == Modifiers::DARK; }
};

auto c { colors::Bases::GREEN, colors::Modifiers::DARK };

// Why not add a user defined literal for the fun
consteval Color operator""_color(const char*, std::size_t)
{
    // ...
}

auto c = "GREEN DARK"_color;
Enter fullscreen mode Exit fullscreen mode

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.

Thread Thread
 
pgradot profile image
Pierre Gradot

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 :

namespace Colors {

enum class ID {
    GREEN_LIGHT, GREEN_DARK, RED_DARK
};

bool isDark(ID color) {
    return color == ID::GREEN_DARK or color == ID::RED_DARK;
}

}

int main() {
    auto c = Colors::ID::GREEN_DARK;
    Colors::isDark(c);
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
baduit profile image
Lena

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)