Baduit

A young French developper who really likes (modern) C++

About me
22 September 2020

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

by Baduit

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.

1
2
3
4
5
6
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.

1
2
3
4
5
6
7
8
9
10
11
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 :

1
2
3
4
5
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.

1
2
3
4
5
6
7
8
9
10
11
12
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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 :

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 :

1
2
3
4
5
6
7
8
9
10
11
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.

1
2
3
4
5
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
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 :

1
2
3
4
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:

1
2
3
4
5
6
// 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 ?

Sources:

tags: C++ - Modern C++