我听到一些人建议在c++中使用枚举类,因为它们的类型安全。

但这到底是什么意思呢?


c++有两种类型的enum:

枚举类 简单的枚举

下面是几个关于如何声明它们的例子:

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

这两者之间有什么区别?

枚举类——枚举器名称是枚举的本地名称,它们的值不会隐式转换为其他类型(比如另一个枚举或int)。 普通枚举——枚举器名称与枚举及其所在的枚举在同一作用域内 值隐式地转换为整数和其他类型

例子:

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;

}

结论:

枚举类应该是首选的,因为它们较少引起可能导致错误的意外。


与普通枚举相比,使用枚举类的基本优点是您可以为两个不同的枚举使用相同的枚举变量,并且仍然可以解析它们(这已经被OP提到为类型安全)。

如:

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

对于基本枚举,编译器将无法区分红色是指下面语句中的类型Color1还是Color2。

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

来自Bjarne Stroustrup的c++ 11常见问题解答:

The enum classes ("new enums", "strong enums") address three problems with traditional C++ enumerations: conventional enums implicitly convert to int, causing errors when someone does not want an enumeration to act as an integer. conventional enums export their enumerators to the surrounding scope, causing name clashes. the underlying type of an enum cannot be specified, causing confusion, compatibility problems, and makes forward declaration impossible. The new enums are "enum class" because they combine aspects of traditional enumerations (names values) with aspects of classes (scoped members and absence of conversions).

因此,正如其他用户所提到的,“强枚举”将使代码更安全。

“经典”枚举的底层类型应该是一个足够大的整数类型,以适合枚举的所有值;这通常是一个int型。此外,每个枚举类型都应与char或有符号/无符号整数类型兼容。

这是对枚举基础类型必须是什么的广泛描述,因此每个编译器将自行决定经典枚举的基础类型,有时结果可能令人惊讶。

例如,我曾多次看到这样的代码:

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

在上面的代码中,一些天真的编码器认为编译器会将e_my_favorite_fruits值存储为一个无符号的8位类型…但这并没有保证:编译器可能会选择unsigned char或int或short,这些类型中的任何一种都足够大,以适应枚举中看到的所有值。添加字段e_my_favorite_fruits_force8是一个负担,它不会强迫编译器对枚举的底层类型做出任何类型的选择。

如果有一段代码依赖于类型大小和/或假设e_my_favorite_fruits有一定的宽度(例如:序列化例程),这段代码可能会根据编译器的想法以一些奇怪的方式表现。

更糟糕的是,如果某个同事不小心在我们的枚举中添加了一个新值:

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

编译器不会抱怨它!它只是调整类型的大小以适应枚举的所有值(假设编译器正在使用尽可能小的类型,这是一个我们不能做的假设)。对枚举的这个简单而粗心的添加可能会微妙地破坏相关代码。

由于c++ 11可以为枚举和枚举类指定底层类型(感谢rdb),所以这个问题被巧妙地解决了:

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

指定底层类型,如果字段的表达式超出了此类型的范围,编译器将报错,而不是更改底层类型。

我认为这是一个很好的安全改进。

那么为什么枚举类优先于普通enum?如果我们可以为有作用域的(enum类)和无作用域的(enum)枚举选择底层类型,还有什么让枚举类成为更好的选择?:

它们不会隐式地转换为int。 它们不会污染周围的名称空间。 它们可以提前宣布。


枚举用于表示一组整数值。

枚举后面的class关键字指定枚举是强类型的,并且枚举数是有作用域的。这样枚举类可以防止意外误用常量。

例如:

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

在这里,我们不能把动物和宠物的价值观混为一谈。

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

c++ 11常见问题包括以下几点:

传统枚举隐式转换为int,当有人不希望枚举充当整数时,会导致错误。

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

传统枚举将枚举数导出到周围的作用域,从而导致名称冲突。

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

无法指定枚举的底层类型,导致混淆、兼容性问题,并无法进行前向声明。

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

因为,正如在其他答案中所说,类enum不能隐式转换为int/bool,这也有助于避免有bug的代码,如:

enum MyEnum {
  Value1,
  Value2,
};
...
if (var == Value1 || Value2) // Should be "var == Value2" no error/warning

还有一件事没有明确提到——作用域特性提供了一个选项,可以让枚举和类方法具有相同的名称。例如:

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

不隐式转换为int 可以选择哪种类型的底层 ENUM命名空间,避免污染发生 与普通类相比,可以向前声明,但没有方法


值得注意的是,在这些答案之上,c++ 20解决了枚举类的一个问题:冗长。想象一个假设的枚举类Color。

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

与普通枚举变体相比,这是详细的,其中名称在全局作用域中,因此不需要用Color::作为前缀。

然而,在c++ 20中,我们可以使用using enum将枚举中的所有名称引入到当前作用域,从而解决了这个问题。

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

所以现在,没有理由不使用枚举类。