¿Por qué se prefiere la clase enum sobre la simple enum?

429

Escuché a algunas personas recomendando usar clases enum en C ++ debido a su tipo de seguridad .

Pero, ¿qué significa esto realmente?

Oleksiy
fuente
57
Cuando alguien afirma que una construcción de programación es "malvada", está tratando de disuadirlo de que piense por sí mismo.
Pete Becker
3
@NicolBolas: Esta es más una pregunta retórica para proporcionar una respuesta de preguntas frecuentes (si esto es realmente una pregunta frecuente es una historia diferente).
David Rodríguez - dribeas
@David, hay una discusión sobre si esto debería ser una pregunta frecuente o no, que comienza aquí . Entrada de bienvenida.
sbi
17
@PeteBecker A veces simplemente intentan protegerte de ti mismo.
piccy
geeksforgeeks.org/... Este es también un lugar bueno para entender enumvs enum class.
mr_azad

Respuestas:

473

C ++ tiene dos tipos de enum:

  1. enum classes
  2. Plain enums

Aquí hay un par de ejemplos de cómo declararlos:

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

¿Cuál es la diferencia entre dos?

  • enum classes - los nombres de los enumeradores son locales para la enumeración y sus valores no se convierten implícitamente a otros tipos (como otro enumo int)

  • Sin formato enum: donde los nombres de los enumeradores están en el mismo ámbito que la enumeración y sus valores se convierten implícitamente en enteros y otros tipos

Ejemplo:

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;

}

Conclusión:

enum classDeben preferirse porque causan menos sorpresas que podrían conducir a errores.

Oleksiy
fuente
77
Buen ejemplo ... ¿hay alguna manera de combinar la seguridad de tipo de la versión de clase con la promoción de espacio de nombres de la versión enum? Es decir, si tengo una clase Acon estado y creo una enum class State { online, offline };como hija de clase A, me gustaría hacer state == onlinecontroles dentro de en Alugar de state == State::online... ¿es eso posible?
marca el
31
No La promoción del espacio de nombres es una mala cosa ™ y la mitad de la justificación enum classera eliminarla.
Cachorro
10
En C ++ 11, también puede usar enumeraciones escritas explícitamente, como enum Animal: unsigned int {dog, deer, cat, bird}
Blasius Secundus
3
@Cat Plus Plus Entiendo que @Oleksiy dice que es malo. Mi pregunta no era si Oleksiy pensaba que era malo. Mi pregunta fue una solicitud para detallar qué tiene de malo. Específicamente, ¿por qué Oleksiy, por ejemplo, se considera malo Color color = Color::red?
chux - Restablece a Mónica el
99
@Cat Plus Plus Entonces, el ejemplo incorrecto no ocurre hasta la if (color == Card::red_card)línea, 4 líneas más tarde que el comentario (que veo ahora se aplica a la primera mitad del bloque). 2 líneas del bloque dan los ejemplos incorrectos . Las primeras 3 líneas no son un problema. El "bloqueo completo es por qué las enumeraciones simples son malas" me arrojó, ya que pensé que querías decir que algo andaba mal con ellos también. Ahora veo, es solo un montaje. En cualquier caso, gracias por los comentarios.
chux - Restablece a Mónica el
248

De las preguntas frecuentes de C ++ 11 de Bjarne Stroustrup :

Los enum classes ("nuevas enumeraciones", "enumeraciones fuertes") abordan tres problemas con las enumeraciones tradicionales de C ++:

  • las enumeraciones convencionales se convierten implícitamente en int, causando errores cuando alguien no quiere que una enumeración actúe como un entero.
  • las enumeraciones convencionales exportan sus enumeradores al ámbito circundante, lo que provoca conflictos de nombres.
  • el tipo subyacente de un enumno se puede especificar, lo que causa confusión, problemas de compatibilidad y hace imposible la declaración directa.

Las nuevas enumeraciones son "enum class" porque combinan aspectos de enumeraciones tradicionales (valores de nombres) con aspectos de clases (miembros con ámbito y ausencia de conversiones).

Entonces, como lo mencionaron otros usuarios, las "enumeraciones fuertes" harían el código más seguro.

El tipo subyacente de un "clásico" enumserá un tipo entero lo suficientemente grande como para ajustarse a todos los valores de enum; esto suele ser un int. Además, cada tipo enumerado debe ser compatible con charun tipo entero con signo / sin signo.

Esta es una descripción amplia de lo enumque debe ser un tipo subyacente, por lo que cada compilador tomará sus propias decisiones sobre el tipo subyacente del clásico enumy, a veces, el resultado podría ser sorprendente.

Por ejemplo, he visto código como este muchas veces:

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?
};

En el código anterior, algunos codificador ingenuo es pensar que el compilador almacenará los E_MY_FAVOURITE_FRUITSvalores en un tipo sin signo de 8 bits ... pero no hay garantía de ello: el compilador puede elegir unsigned charo int, o short, cualquiera de esos tipos son lo suficientemente grandes como para caber toda la valores vistos en el enum. Agregar el campo E_MY_FAVOURITE_FRUITS_FORCE8es una carga y no obliga al compilador a hacer ningún tipo de elección sobre el tipo subyacente deenum .

Si hay algún código que dependa del tamaño de letra y / o asuma que E_MY_FAVOURITE_FRUITS letra sería de cierto ancho (por ejemplo: rutinas de serialización), este código podría comportarse de maneras extrañas dependiendo de los pensamientos del compilador.

Y para empeorar las cosas, si algún compañero de trabajo agrega descuidadamente un nuevo valor a nuestro enum:

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

¡El compilador no se queja! Simplemente cambia el tamaño del tipo para que se ajuste a todos los valores del enum(suponiendo que el compilador estaba usando el tipo más pequeño posible, lo cual es una suposición que no podemos hacer). Esta adición simple y descuidada a laenum podría romper sutilmente el código relacionado.

Dado que C ++ 11 es posible especificar el tipo subyacente para enumy enum class(gracias rdb ), este problema se soluciona perfectamente:

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
};

Especificando el tipo subyacente si un campo tiene una expresión fuera del rango de este tipo, el compilador se quejará en lugar de cambiar el tipo subyacente.

Creo que esta es una buena mejora de seguridad.

Entonces, ¿ por qué se prefiere la clase enum sobre la simple enum? , si podemos elegir el tipo subyacente para las enum classenumeraciones scoped ( enum) y unscoped ( ), ¿qué más hace enum classuna mejor elección ?:

  • No se convierten implícitamente a int .
  • No contaminan el espacio de nombres circundante.
  • Pueden ser declarados hacia adelante.
PaperBirdMaster
fuente
1
Supongo que también podemos restringir el tipo de base de enumeración para enumeraciones regulares, siempre que tengamos C ++ 11
Sagar Padhye
11
Lo siento, pero esta respuesta es incorrecta. "enum class" no tiene nada que ver con la capacidad de especificar el tipo. Esa es una característica independiente que existe tanto para las enumeraciones regulares como para las clases de enumeración.
rdb
14
Este es el trato: * Las clases Enum son una nueva característica en C ++ 11. * Las enumeraciones escritas son una nueva característica en C ++ 11. Estas son dos nuevas características independientes no relacionadas en C ++ 11. Puede usar ambos, o puede usar cualquiera, o ninguno.
rdb
2
Creo que Alex Allain proporciona la explicación simple más completa que he visto en este blog en [ cprogramming.com/c++11/… . La enumeración tradicional era buena para usar nombres en lugar de valores enteros y evitar el uso del preprocesador #defines, que era una buena cosa: agregaba claridad. la clase enum elimina el concepto de un valor numérico del enumerador e introduce el alcance y la tipificación fuerte que aumenta (bueno, puede aumentar :-) la corrección del programa. Te acerca un paso más al pensamiento orientado a objetos.
Jon Spencer
2
Por otro lado, siempre es divertido cuando revisas el código y de repente sucede One Piece .
Justin Time - Restablece a Monica el
47

La ventaja básica de usar la clase enum sobre las enumeraciones normales es que puede tener las mismas variables de enumeración para 2 enumeraciones diferentes y aún así puede resolverlas (lo cual ha sido mencionado como tipo seguro por OP)

Por ejemplo:

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 cuanto a las enumeraciones básicas, el compilador no podrá distinguir si redse refiere al tipo Color1o Color2como se muestra en la siguiente declaración.

enum Color1 { red, green, blue };   
enum Color2 { red, green, blue };
int x = red;    //Compile time error(which red are you refering to??)
Saksham
fuente
1
@Oleksiy Ohh, no leí tu pregunta correctamente. Considerar es como un complemento para aquellos que no lo sabían.
Saksham
¡está bien! Casi me olvido de esto
Oleksiy
por supuesto, escribirías enum { COLOR1_RED, COLOR1_GREE, COLOR1_BLUE }, obviando fácilmente los problemas de espacio de nombres. El argumento del espacio de nombres es uno de los tres mencionados aquí que no compro en absoluto.
Jo So
2
@Jo Entonces, esa solución es una solución innecesaria. Enumeración: enum Color1 { COLOR1_RED, COLOR1_GREEN, COLOR1_BLUE }es comparable a la clase de enumeración: enum class Color1 { RED, GREEN, BLUE }. El acceso es similar: COLOR1_REDvs Color1::RED, pero la versión Enum requiere que escriba "COLOR1" en cada valor, lo que da más espacio para errores tipográficos, lo que evita el comportamiento del espacio de nombres de una clase enum.
cdgraham
2
Por favor, use la crítica constructiva . Cuando digo más espacio para los errores tipográficos, quiero decir cuando definiste originalmente los valores de enum Color1, que un compilador no puede detectar, ya que probablemente todavía sería un nombre "válido". Si escribo RED, GREENy así sucesivamente usando una clase enum, entonces no puede resolverlo enum Bananaporque requiere que especifiques Color1::REDpara acceder al valor (el argumento del espacio de nombres). Todavía hay buenos momentos para usar enum, pero el comportamiento del espacio de nombres de un a enum classmenudo puede ser muy beneficioso.
cdgraham
20

Las enumeraciones se utilizan para representar un conjunto de valores enteros.

La classpalabra clave después de enumespecifica que la enumeración está fuertemente tipada y sus enumeradores tienen un alcance. De esta forma, las enumclases evitan el mal uso accidental de las constantes.

Por ejemplo:

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

Aquí no podemos mezclar los valores de animales y mascotas.

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

Las preguntas frecuentes de C ++ 11 mencionan los siguientes puntos:

las enumeraciones convencionales se convierten implícitamente en int, causando errores cuando alguien no quiere que una enumeración actúe como un entero.

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;
}

las enumeraciones convencionales exportan sus enumeradores al ámbito circundante, lo que provoca conflictos de nombres.

// 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
};

El tipo subyacente de una enumeración no se puede especificar, lo que causa confusión, problemas de compatibilidad y hace imposible la declaración directa.

// 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
fuente
C ++ 11 permite escribir también enumeraciones "no de clase" . Los problemas de contaminación del espacio de nombres, etc., todavía existen. Eche un vistazo a las respuestas relevantes que existieron mucho antes de esta ...
user2864740
7
  1. no convertir implícitamente a int
  2. puede elegir qué tipo subyace
  3. Espacio de nombres ENUM para evitar que sucedan contaminantes
  4. En comparación con la clase normal, puede declararse hacia adelante, pero no tiene métodos
Qinsheng Zhang
fuente
2

Vale la pena señalar, además de estas otras respuestas, que C ++ 20 resuelve uno de los problemas que enum classtiene: verbosidad. Imaginando un hipotético enum class, Color.

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

Esto es detallado en comparación con la enumvariación simple , donde los nombres están en el ámbito global y, por lo tanto, no necesitan tener el prefijo Color::.

Sin embargo, en C ++ 20 podemos usar using enumpara introducir todos los nombres en una enumeración al alcance actual, resolviendo el problema.

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

Así que ahora, no hay razón para no usar enum class.

Tom VH
fuente
1

Porque, como se dijo en otras respuestas, la enumeración de la clase no es implícitamente convertible a int / bool, también ayuda a evitar códigos con errores como:

enum MyEnum {
  Value1,
  Value2,
};
...
if (var == Value1 || Value2) // Should be "var == Value2" no error/warning
Arnaud
fuente
2
Para completar mi comentario anterior, tenga en cuenta que gcc ahora tiene una advertencia llamada -Wint-in-bool-context que detectará exactamente este tipo de errores.
Arnaud
0

Una cosa que no se ha mencionado explícitamente: la función de alcance le brinda la opción de tener el mismo nombre para un método de enumeración y clase. Por ejemplo:

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
fuente