Pourquoi la classe enum est-elle préférée à la simple énumération?

429

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

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

Oleksiy
la source
57
Quand quelqu'un prétend que certaines constructions de programmation sont "mauvaises", il essaie de vous décourager de penser par vous-même.
Pete Becker
3
@NicolBolas: Il s'agit plus d'une question réthorique pour fournir une réponse à la FAQ (que ce soit vraiment Frequenty demandé, c'est une autre histoire).
David Rodríguez - dribeas
@David, il y a une discussion pour savoir si cela devrait être une FAQ ou non qui commence ici . Entrée bienvenue.
sbi
17
@PeteBecker Parfois, ils essaient simplement de vous protéger de vous-même.
piccy
geeksforgeeks.org/... C'est aussi un bon endroit pour comprendre enumvs enum class.
mr_azad

Réponses:

473

C ++ a deux types de enum:

  1. enum classes
  2. Plaine 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 locaux à l'énumération et leurs valeurs ne sont pas implicitement converties en d'autres types (comme un autre enumou int)

  • Plain enums - où les noms d'énumérateur ont la même portée que l'énumération et leurs valeurs sont implicitement converties 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 doivent être privilégiées car elles provoquent moins de surprises pouvant potentiellement conduire à des bugs.

Oleksiy
la source
7
Bon exemple ... existe-t-il un moyen de combiner la sécurité de type de la version de classe avec la promotion d'espace de noms de la version enum? Autrement dit, si j'ai une classe Aavec état et que je crée un en enum class State { online, offline };tant qu'enfant de classe A, j'aimerais faire des state == onlinevérifications à l'intérieur de Aau lieu de state == State::online... est-ce possible?
marquez
31
Nan. La promotion de l'espace de noms est une mauvaise chose ™ et la moitié de la justification enum classétait de l'éliminer.
Chiot
10
En C ++ 11, vous pouvez également utiliser des énumérations typées explicitement, comme enum Animal: unsigned int {chien, cerf, chat, oiseau}
Blasius Secundus
3
@Cat Plus Plus Je comprends que @Oleksiy dit que c'est mauvais. Ma question n'était pas de savoir si Oleksiy pensait que c'était mauvais. Ma question était une demande de détailler ce qui est mauvais à ce sujet. Plus précisément, pourquoi Oleksiy, par exemple, considère-t-il mauvais Color color = Color::red.
chux
9
@Cat Plus Plus Donc, le mauvais exemple ne se produit pas avant la if (color == Card::red_card)ligne, 4 lignes plus tard que le commentaire (que je vois maintenant s'applique à la première moitié du bloc.) 2 lignes du bloc donnent les mauvais exemples. Les 3 premières lignes ne sont pas un problème. Le «bloc entier est la raison pour laquelle les énumérations simples sont mauvaises» m'a jeté alors que je pensais que vous vouliez dire que quelque chose n'allait pas avec celles-ci aussi. Je vois maintenant, c'est juste une mise en place. Dans tous les cas, merci pour les commentaires.
chux
248

De la FAQ C ++ 11 de Bjarne Stroustrup :

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

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

Les nouvelles énumérations sont «enum class» car elles combinent des aspects d'énumérations traditionnelles (valeurs de noms) avec des aspects de classes (membres de portée et absence de conversions).

Ainsi, comme mentionné par d'autres utilisateurs, les «énumérations fortes» rendraient le code plus sûr.

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

Il s'agit d'une description large de ce que enumdoit être un type sous-jacent, de sorte que chaque compilateur décidera seul du type sous-jacent du classiqueenum et parfois le résultat pourrait être surprenant.

Par exemple, j'ai vu du 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 E_MY_FAVOURITE_FRUITSvaleurs dans un type 8 bits non signé ... mais il n'y a aucune garantie: le compilateur peut choisir unsigned charou intou short, n'importe lequel de ces types est assez grand pour s'adapter à tous les valeurs vues dans le enum. L'ajout du champ E_MY_FAVOURITE_FRUITS_FORCE8est un fardeau et n'oblige pas le compilateur à faire un choix quelconque sur le type sous-jacent du enum.

S'il y a un morceau de code qui dépend de la taille du type et / ou suppose qu'il E_MY_FAVOURITE_FRUITSserait d'une certaine largeur (par exemple: routines de sérialisation), ce code pourrait se comporter de manière étrange selon les pensées 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 s'adapter à toutes les valeurs de la enum(en supposant que le compilateur utilisait le plus petit type possible, ce qui est une hypothèse que nous ne pouvons pas faire). Cet ajout simple et imprudent au enumcode pourrait briser la subtilité.

Depuis C ++ 11 est possible de spécifier le type sous-jacent pour enumet enum class(merci rdb ) donc ce problème est soigneusement 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 hors 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é.

Alors pourquoi la classe enum est-elle préférée à la simple énumération? , si nous pouvons choisir le type sous-jacent pour les énumérations scoped ( enum class) et unscoped ( enum), quoi d'autre fait enum classun meilleur choix?:

  • Ils ne se convertissent pas implicitement en int.
  • Ils ne polluent pas l'espace de noms environnant.
  • Ils peuvent être déclarés à l'avance.
PaperBirdMaster
la source
1
Je suppose que nous pouvons également restreindre le type de base enum pour les énumérations régulières, tant que nous avons C ++ 11
Sagar Padhye
11
Désolé, mais cette réponse est fausse. "enum class" n'a rien à voir avec la possibilité de spécifier le type. C'est une fonctionnalité indépendante qui existe à la fois pour les énumérations régulières et pour les classes d'énumérations.
rdb
14
Voici l'affaire: * Les classes Enum sont une nouvelle fonctionnalité de C ++ 11. * Les énumérations typées sont une nouvelle fonctionnalité de C ++ 11. Ce sont deux nouvelles fonctionnalités distinctes non liées dans C ++ 11. Vous pouvez utiliser les deux, ou vous pouvez utiliser l'un ou l'autre.
rdb
2
Je pense qu'Alex Allain fournit l' explication simple la plus complète que j'ai jamais vue dans ce blog à [ cprogramming.com/c++11/… . L' énumération traditionnelle était bonne pour utiliser des noms au lieu de valeurs entières et éviter d'utiliser le préprocesseur #defines, ce qui était une bonne chose - cela a ajouté de la clarté. La classe enum supprime le concept d'une valeur numérique de l'énumérateur, et introduit la portée et le typage fort qui augmente (enfin, peut augmenter :-) l'exactitude du programme. Il vous rapproche d'un pas de la pensée orientée objet.
Jon Spencer
2
En passant, c'est toujours amusant quand vous passez en revue le code et soudainement One Piece se produit.
Justin Time - Rétablir Monica le
47

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

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 s'il redfait référence au type Color1ou Color2comme 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??)
Saksham
la source
1
@Oleksiy Ohh, je n'ai pas bien lu votre question. Considérez-le comme un complément pour ceux qui ne le savaient pas.
Saksham
c'est bon! J'ai presque oublié ça
Oleksiy
bien sûr, vous écririez enum { COLOR1_RED, COLOR1_GREE, COLOR1_BLUE }, évitant facilement les problèmes d'espace de noms. L'argument d'espace de noms est l'un des trois mentionnés ici que je n'achète pas du tout.
Jo So
2
@Jo So Cette solution est une solution de contournement inutile. Enum: enum Color1 { COLOR1_RED, COLOR1_GREEN, COLOR1_BLUE }est comparable à la classe Enum: enum class Color1 { RED, GREEN, BLUE }. L'accès est similaire: COLOR1_REDvs Color1::RED, mais la version Enum nécessite que vous tapiez "COLOR1" dans chaque valeur, ce qui donne plus de place aux fautes de frappe, ce que le comportement d'espace de noms d'une classe enum évite.
cdgraham
2
Veuillez utiliser une critique constructive . Quand je dis plus de place pour les fautes de frappe, je veux dire quand vous définissez à l'origine les valeurs de enum Color1, qu'un compilateur ne peut pas attraper car ce serait probablement encore un nom «valide». Si j'écris RED, GREENet ainsi de suite en utilisant une classe enum, cela ne peut pas être résolu enum Bananacar il nécessite que vous le spécifiiez Color1::REDpour accéder à la valeur (l'argument d'espace de noms). Il y a encore de bons moments à utiliser enum, mais le comportement d'espace de noms d'un enum classpeut souvent être très bénéfique.
cdgraham
20

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

Le classmot-clé après le enumspécifie que l'énumération est fortement typée et ses énumérateurs sont limités. De cette façon, les enumclasses empêchent une mauvaise utilisation accidentelle des constantes.

Par exemple:

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

Ici, nous ne pouvons pas mélanger les valeurs des animaux et des animaux domestiques.

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

La FAQ C ++ 11 mentionne les points ci-dessous:

les énumérations conventionnelles sont implicitement converties en int, provoquant 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 dans la portée environnante, provoquant 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 de la confusion, des problèmes de compatibilité et rend la déclaration directe impossible.

// 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;
}
Swapnil
la source
C ++ 11 permet énumérations « non-classe » à tapées ainsi. Les problèmes de pollution des espaces de noms, etc., existent toujours. Jetez un œil aux réponses pertinentes qui existaient bien avant celle-ci.
user2864740
7
  1. ne pas convertir implicitement en int
  2. peut choisir le type sous-jacent
  3. Espace de noms ENUM pour éviter la pollution
  4. Comparé à la classe normale, peut être déclaré en avant, mais n'a pas de méthode
Qinsheng Zhang
la source
2

Il convient de noter, en plus de ces autres réponses, que C ++ 20 résout l'un des problèmes qui enum classa: la verbosité. Imaginer un hypothétique enum class, Color.

void foo(Color c)
  switch (c) {
    case Color::Red: ...;
    case Color::Green: ...;
    case Color::Blue: ...;
    // etc
  }
}

Ceci est détaillé par rapport à la enumvariante simple , où les noms sont dans la portée globale et n'ont donc pas besoin d'être préfixés avecColor:: .

Cependant, en C ++ 20, nous pouvons utiliser using enumpour introduire tous les noms d'une énumération dans la portée actuelle, en résolvant le problème.

void foo(Color c)
  using enum Color;
  switch (c) {
    case Red: ...;
    case Green: ...;
    case Blue: ...;
    // etc
  }
}

Alors maintenant, il n'y a aucune raison de ne pas l'utiliser enum class.

Tom VH
la source
1

Parce que, comme indiqué dans d'autres réponses, les énumérations de classe ne sont pas implicitement convertibles en int / bool, cela permet également d'éviter le code bogué comme:

enum MyEnum {
  Value1,
  Value2,
};
...
if (var == Value1 || Value2) // Should be "var == Value2" no error/warning
Arnaud
la source
2
Pour terminer mon commentaire précédent, notez que gcc a maintenant un avertissement appelé -Wint-in-bool-context qui détectera exactement ce type d'erreurs.
Arnaud
0

Une chose qui n'a pas été explicitement mentionnée - la fonctionnalité de portée vous donne la possibilité d'avoir le même nom pour une méthode enum et class. 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
};
Miro Kropacek
la source