web-dev-qa-db-fra.com

Pourquoi la classe enum est-elle préférée au plain enum?

J'ai entendu quelques personnes recommander d'utiliser des classes enum en C++ en raison de leur type safety.

Mais qu'est-ce que cela signifie réellement?

357
Oleksiy

C++ a deux types de enum:

  1. enum classes
  2. Plain enums

Voici quelques exemples pour les déclarer:

 enum class Color { red, green, blue }; // enum class
 enum Animal { dog, cat, bird, human }; // plain enum 

Quelle est la différence entre deux?

  • enum classes - les noms d’énumérateur sont local enum et leurs valeurs ne ( ne sont pas converties implicitement vers d’autres types (comme un autre enum ou int)

  • Plain enums - où les noms d’énumérateur ont la même portée que l’énumération et dont les valeurs sont converties implicitement en entiers et autres types

Exemple:

enum Color { red, green, blue };                    // plain enum 
enum Card { red_card, green_card, yellow_card };    // another plain enum 
enum class Animal { dog, deer, cat, bird, human };  // enum class
enum class Mammal { kangaroo, deer, human };        // another enum class

void fun() {

    // examples of bad use of plain enums:
    Color color = Color::red;
    Card card = Card::green_card;

    int num = color;    // no problem

    if (color == Card::red_card) // no problem (bad)
        cout << "bad" << endl;

    if (card == Color::green)   // no problem (bad)
        cout << "bad" << endl;

    // examples of good use of enum classes (safe)
    Animal a = Animal::deer;
    Mammal m = Mammal::deer;

    int num2 = a;   // error
    if (m == a)         // error (good)
        cout << "bad" << endl;

    if (a == Mammal::deer) // error (good)
        cout << "bad" << endl;

}

Conclusion:

enum classes devraient être préférés car ils causent moins de surprises pouvant potentiellement conduire à des bugs.

394
Oleksiy

De FAQ de C++ 11 de Bjarne Stroustrup :

Les enum classes ("nouvelles énumérations", "fortes énumérations") résolvent trois problèmes avec les énumérations C++ traditionnelles:

  • les énumérations conventionnelles sont converties implicitement en int, ce qui provoque des erreurs lorsque quelqu'un ne veut pas qu'une énumération agisse comme un entier.
  • les énumérations conventionnelles exportent leurs énumérateurs vers la portée environnante, ce qui provoque des conflits de noms.
  • le type sous-jacent de enum ne peut pas être spécifié, ce qui entraîne une confusion, des problèmes de compatibilité et rend impossible la déclaration en aval.

Les nouveaux enum sont "enum class" car ils combinent des aspects des énumérations traditionnelles (valeurs de noms) avec des aspects des classes (membres définis et absence de conversions).

Ainsi, comme mentionné par d'autres utilisateurs, les "enums forts" rendraient le code plus sûr.

Le type sous-jacent d'un "classique" enum doit être un type entier suffisamment grand pour contenir toutes les valeurs de enum; c'est généralement un int. De plus, chaque type énuméré doit être compatible avec char ou un type entier signé/non signé.

Il s'agit d'une description large de ce que doit être un type sous-jacent enum. Ainsi, chaque compilateur prendra lui-même les décisions relatives au type sous-jacent du classique enum et le résultat pourrait parfois être surprenant.

Par exemple, j'ai vu un code comme celui-ci plusieurs fois:

enum E_MY_FAVOURITE_FRUITS
{
    E_Apple      = 0x01,
    E_WATERMELON = 0x02,
    E_COCONUT    = 0x04,
    E_STRAWBERRY = 0x08,
    E_CHERRY     = 0x10,
    E_PINEAPPLE  = 0x20,
    E_BANANA     = 0x40,
    E_MANGO      = 0x80,
    E_MY_FAVOURITE_FRUITS_FORCE8 = 0xFF // 'Force' 8bits, how can you tell?
};

Dans le code ci-dessus, un codeur naïf pense que le compilateur stockera les valeurs E_MY_FAVOURITE_FRUITS dans un type 8 bits non signé ... mais il n'y a aucune garantie à ce sujet: le compilateur peut choisir unsigned char ou int ou short , l’un ou l’autre de ces types est suffisamment grand pour s’adapter à toutes les valeurs indiquées dans enum. L'ajout du champ E_MY_FAVOURITE_FRUITS_FORCE8 est un fardeau et n'oblige pas le compilateur à faire un choix sur le type sous-jacent de enum.

S'il existe des éléments de code qui dépendent de la taille du type et/ou supposent que E_MY_FAVOURITE_FRUITS aurait une certaine largeur (par exemple: routines de sérialisation), ce code pourrait se comporter de manière étrange en fonction des réflexions du compilateur.

Et pour aggraver les choses, si un collègue ajoute négligemment une nouvelle valeur à notre enum:

    E_DEVIL_FRUIT  = 0x100, // New fruit, with value greater than 8bits

Le compilateur ne s'en plaint pas! Il redimensionne simplement le type pour qu'il corresponde à toutes les valeurs de enum (en supposant que le compilateur utilisait le type le plus petit possible, ce qui est une hypothèse que nous ne pouvons pas faire). Cette addition simple et négligente à la variable enum pourrait rompre subtilement le code associé.

Comme C++ 11 est possible de spécifier le type sous-jacent pour enum et enum class (merci rdb ), le problème est donc résolu:

enum class E_MY_FAVOURITE_FRUITS : unsigned char
{
    E_Apple        = 0x01,
    E_WATERMELON   = 0x02,
    E_COCONUT      = 0x04,
    E_STRAWBERRY   = 0x08,
    E_CHERRY       = 0x10,
    E_PINEAPPLE    = 0x20,
    E_BANANA       = 0x40,
    E_MANGO        = 0x80,
    E_DEVIL_FRUIT  = 0x100, // Warning!: constant value truncated
};

En spécifiant le type sous-jacent si un champ a une expression en dehors de la plage de ce type, le compilateur se plaindra au lieu de changer le type sous-jacent.

Je pense que c'est une bonne amélioration de la sécurité.

Donc Pourquoi la classe enum est-elle préférée au type enum? , si nous pouvons choisir le type sous-jacent pour scoped (enum class) et non-ciblé (enum) enums quoi d’autre fait de enum class un meilleur choix ?:

  • Ils ne convertissent pas implicitement en int.
  • Ils ne polluent pas l'espace de noms environnant.
  • Ils peuvent être déclarés en avant.
214
PaperBirdMaster

L'avantage de base de l'utilisation de enum class par rapport aux énumérations normales est que vous pouvez avoir les mêmes variables d'enum pour 2 énumérations différentes et que vous pouvez toujours les résoudre (ce qui a été mentionné comme type safe par OP)

Pour par exemple:

enum class Color1 { red, green, blue };    //this will compile
enum class Color2 { red, green, blue };

enum Color1 { red, green, blue };    //this will not compile 
enum Color2 { red, green, blue };

En ce qui concerne les énumérations de base, le compilateur ne pourra pas distinguer si red fait référence au type Color1 ou Color2 comme dans l’instruction ci-dessous.

enum Color1 { red, green, blue };   
enum Color2 { red, green, blue };
int x = red;    //Compile time error(which red are you refering to??)
41
Saksham

Les énumérations sont utilisées pour représenter un ensemble de valeurs entières.

Le mot-clé class après le enum spécifie que l'énumération est fortement typée et que ses énumérateurs sont étendus. De cette façon, enum classes prévient les mauvaises utilisations accidentelles des constantes.

Par exemple:

enum class Animal{Dog, Cat, Tiger};
enum class Pets{Dog, Parrot};

Ici, nous ne pouvons pas mélanger les valeurs animales et animales.

Animal a = Dog;       // Error: which DOG?    
Animal a = Pets::Dog  // Pets::Dog is not an Animal
19
Alok151290

FAQ C++ 11 mentionne les points suivants:

Les énumérations conventionnelles sont converties implicitement en int, ce qui provoque des erreurs lorsque quelqu'un ne veut pas qu'une énumération agisse comme un entier.

enum color
{
    Red,
    Green,
    Yellow
};

enum class NewColor
{
    Red_1,
    Green_1,
    Yellow_1
};

int main()
{
    //! Implicit conversion is possible
    int i = Red;

    //! Need enum class name followed by access specifier. Ex: NewColor::Red_1
    int j = Red_1; // error C2065: 'Red_1': undeclared identifier

    //! Implicit converison is not possible. Solution Ex: int k = (int)NewColor::Red_1;
    int k = NewColor::Red_1; // error C2440: 'initializing': cannot convert from 'NewColor' to 'int'

    return 0;
}

Les énumérations conventionnelles exportent leurs énumérateurs vers la portée environnante, ce qui provoque des conflits de noms.

// Header.h

enum vehicle
{
    Car,
    Bus,
    Bike,
    Autorickshow
};

enum FourWheeler
{
    Car,        // error C2365: 'Car': redefinition; previous definition was 'enumerator'
    SmallBus
};

enum class Editor
{
    vim,
    eclipes,
    VisualStudio
};

enum class CppEditor
{
    eclipes,       // No error of redefinitions
    VisualStudio,  // No error of redefinitions
    QtCreator
};

Le type sous-jacent d'une énumération ne peut pas être spécifié, ce qui provoque une confusion, des problèmes de compatibilité et rend impossible la déclaration en aval.

// Header1.h
#include <iostream>

using namespace std;

enum class Port : unsigned char; // Forward declare

class MyClass
{
public:
    void PrintPort(enum class Port p);
};

void MyClass::PrintPort(enum class Port p)
{
    cout << (int)p << endl;
}

.

// Header.h
enum class Port : unsigned char // Declare enum type explicitly
{
    PORT_1 = 0x01,
    PORT_2 = 0x02,
    PORT_3 = 0x04
};

.

// Source.cpp
#include "Header1.h"
#include "Header.h"

using namespace std;
int main()
{
    MyClass m;
    m.PrintPort(Port::PORT_1);

    return 0;
}
4
Swapnil

Comme, comme indiqué dans d'autres réponses, class enum ne peut pas être converti implicitement en int/bool, il est également utile d'éviter un code erroné tel que:

enum MyEnum {
  Value1,
  Value2,
};
...
if (var == Value1 || Value2) // Should be "var == Value2" no error/warning
1
Arnaud
  1. ne pas convertir implicitement en int
  2. peut choisir quel type sous-jacent
  3. Espace de noms ENUM pour éviter la pollution
  4. Par rapport à la classe normale, peuvent être déclarés en avant, mais n'ont pas de méthodes
1
Elon Zhang

Une chose qui n’a pas été explicitement mentionnée - la fonctionnalité scope vous donne la possibilité d’avoir le même nom pour une méthode enum et une classe. Par exemple:

class Test
{
public:
   // these call ProcessCommand() internally
   void TakeSnapshot();
   void RestoreSnapshot();
private:
   enum class Command // wouldn't be possible without 'class'
   {
        TakeSnapshot,
        RestoreSnapshot
   };
   void ProcessCommand(Command cmd); // signal the other thread or whatever
};
0
Miro Kropacek