为什么枚举类被认为比普通枚举更安全?

712
我听说有些人推荐在C++中使用枚举类,因为它们具有类型安全性。
但是这到底意味着什么呢?

119
当有人声称某种编程结构是“邪恶”的时候,他们试图阻止你独立思考。 - Pete Becker
5
@NicolBolas:这更多是一个修辞问题,以提供常见问题解答(这是否真正经常被问到是另一回事)。 - David Rodríguez - dribeas
1
@David,有一个讨论正在进行中,讨论是否应该将此作为常见问题解答。讨论从这里开始:http://chat.stackoverflow.com/transcript/message/11355349#11355349。欢迎提供意见。 - sbi
40
有时候他们只是试图保护你不受自己的伤害。 - piccy
1
这是一个了解C++中enumenum class区别的好地方。https://www.geeksforgeeks.org/enum-classes-in-c-and-their-advantage-over-enum-datatype/ - herr_azad
显示剩余2条评论
9个回答

788

C++有两种枚举类型:enum class和普通的enum

  1. enum class类型的枚举
  2. 普通的enum类型的枚举

以下是它们的声明示例:

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

这两者之间有什么区别?

  • enum class - 枚举器名称是局部的,它们的值不会隐式转换为其他类型(如另一个enumint

  • 普通的enum - 枚举器名称与枚举器在同一作用域中,它们的值会隐式转换为整数和其他类型

例如:

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;

}

结论:

enum class应该更受青睐,因为它们会导致更少的意外情况,这些意外情况可能会导致错误。


13
好的例子...有没有一种方法将类版本的类型安全与枚举版本的名称空间提升相结合?也就是说,如果我有一个带状态的类A,并且我创建一个作为A类子元素的enum class State { online, offline };,我想在A内部进行state == online检查,而不是state == State::online...这可行吗? - mark
56
不行。命名空间提升是一件不好的事情™,而enum class的其中一项正当理由就是消除它。 - Puppy
27
在C++11中,您也可以使用显式类型枚举,例如 enum Animal: unsigned int {dog, deer, cat, bird}。 - Blasius Secundus
8
我明白@Cat Plus Plus的话,他说@Oleksiy觉得这样不好。我的问题并不是问Oleksiy是否认为它不好,而是请求详细说明有关此事的问题。具体来说,为什么例如Color color = Color::red这样会被Oleksiy认为不好? - chux - Reinstate Monica
11
因此,示例中的“错误”直到if (color == Card::red_card)这一行出现,比注释晚了4行(我现在看到注释只适用于块的前半部分)。该块的2行给出了“错误”的示例。前3行没有问题。 “整个块为何纯枚举类型不好”让我困惑,因为我以为您也认为它们有问题。 我现在明白了,这只是一个铺垫。无论如何,感谢您的反馈。 - chux - Reinstate Monica
显示剩余11条评论

324

Bjarne Stroustrup的C++11 FAQ

enum class(“新枚举”,“强枚举”)解决了传统C++枚举的三个问题:

  • 传统枚举会隐式转换为int,当某人不想将枚举类型作为整数时会导致错误。
  • 传统枚举将其枚举器导出到周围的范围,导致名称冲突。
  • 无法指定枚举的基础类型,会引起混乱、兼容性问题,并使前向声明变得不可能。

新的枚举是“枚举类”,因为它们结合了传统枚举(名称值)和类的方面(作用域成员和没有转换)。

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

“经典” enum的基础类型应是足以容纳所有枚举值的整数类型;通常这是一个int。每种枚举类型还应与char或有符号/无符号整数类型兼容。

这是关于enum基础类型的广泛描述,因此每个编译器都将自行决定经典enum的基础类型,有时结果可能令人惊讶。

例如,我已经多次看到过这样的代码:

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_FAVOURITE_FRUITS 的值存储到一个无符号8位类型中... 但是没有任何保证:编译器可能会选择 unsigned char 或者 int 或者 short,这些类型都足够大来适应 enum 中看到的所有值。添加字段 E_MY_FAVOURITE_FRUITS_FORCE8 是一种负担,不会强制编译器对 enum 的底层类型作出任何选择。

如果有一些代码依赖于类型大小和/或假设 E_MY_FAVOURITE_FRUITS 会有某个宽度(例如:序列化例程),那么这段代码可能会因为编译器想法而表现出奇怪的行为。

更糟糕的是,如果一位同事粗心地向我们的 enum 添加了一个新值:

    E_DEVIL_FRUIT  = 0x100, // New fruit, with value greater than 8bits
编译器不会给出错误提示,它只是调整类型的大小以适应所有enum值(假设编译器使用最小可能的类型,这种假设我们无法做到)。这个简单而粗心的添加到enum可能会微妙地破坏相关代码。自从C++11以来,可以为enumenum class指定底层类型(感谢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 class比普通枚举更受推崇呢?如果我们可以为作用域为enum class和非作用域为enum的枚举选择底层类型,还有什么使enum class成为更好的选择呢?:

  • 它们不能隐式转换为int
  • 它们不会污染周围的命名空间。
  • 它们可以被前置声明。

24
不好意思,但是这个回答是错误的。“enum class”与指定类型的能力无关。这是一种独立的特性,既适用于常规枚举,也适用于枚举类。 - rdb
23
这就是问题所在:
  • 枚举类是C++11的一个新特性。
  • 类型化枚举是C++11的一个新特性。 这两个特性是C++11中独立无关的新特性。你可以同时使用它们,也可以只使用其中一个,或者都不使用。
- rdb
3
我认为Alex Allain在[http://www.cprogramming.com/c++11/c++11-nullptr-strongly-typed-enum-class.html]中提供了一个最完整的、简单易懂的解释。传统的枚举类型是用名称代替整数值,避免使用预处理器#define,这是一件好事情——它增加了代码的清晰度。 enum class 删除了枚举变量的数字值概念,并引入了作用域和强类型,可以增加程序正确性(或者说有可能增加)。它让你更接近面向对象的思想。 - Jon Spencer
2
然而,我相信您必须自己定义位运算,这会增加很多样板代码。 - user2672165
2
顺便说一句,当你在审查代码时突然出现《海贼王》时,总是很有趣的。 - Justin Time - Reinstate Monica
显示剩余5条评论

64

使用枚举类而不是普通枚举的基本优势在于,您可以拥有相同的枚举变量用于2个不同的枚举,同时仍然可以解决它们(这已被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 };

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

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

2
@Oleksiy 哦,我没有仔细阅读你的问题。考虑一下这是一个附加功能,供那些不知道的人使用。 - Saksham
1
当然,你可以编写 enum { COLOR1_RED, COLOR1_GREE, COLOR1_BLUE },轻松避免命名空间问题。在这里提到的三个参数中,命名空间参数是我完全不认同的一个。 - Jo So
3
@Jo 那个解决方案是一个不必要的变通方法。枚举 enum Color1 { COLOR1_RED, COLOR1_GREEN, COLOR1_BLUE } 可以与 Enum 类相媲美: enum class Color1 { RED, GREEN, BLUE }。访问方式类似:COLOR1_REDColor1::RED,但是枚举版本需要在每个值中键入“COLOR1”,这给打错字留下了更多空间,而枚举类的命名空间行为避免了这种情况。 - cdgraham
澄清一下:这很让人抓狂,因为如果前缀中有拼写错误,那么编译器有99.99%的机会会捕捉到它。对于剩下的0.01%,程序员可能会注意到,因为程序表现异常。 - Jo So
4
请使用建设性批评。当我说“更多的错别字空间”时,我指的是当您最初定义enum Color1的值时,编译器无法捕获它,因为它仍然可能是一个“有效”的名称。如果我使用枚举类写REDGREEN等,那么它不能解析为enum Banana,因为它需要您指定Color1::RED(命名空间参数)才能访问该值。仍然有一些适合使用enum的场景,但是enum class的命名空间行为通常非常有益。 - cdgraham
显示剩余3条评论

25

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

enum 后面的 class 关键字指定了枚举类是强类型的,其枚举器是有作用域的。这样,enum 类可以防止常量被错误地使用。

例如:

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

21

值得注意的是,在这些其他答案之外,C++20解决了enum class存在的一个问题:冗长。假设有一个假想的enum class,名为Color

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

相对于简单的enum变体,此处内容有些冗长,其中名称位于全局作用域中,因此无需以Color::为前缀。

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

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

现在,没有理由不使用enum class了。


12

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

1
C++11允许“非类”枚举也具有类型。命名空间污染问题等仍然存在。请查看此之前很久就存在的相关答案。 - user2864740

11
  1. 不要隐式转换为int类型
  2. 可以选择其基础类型
  3. 使用ENUM命名空间以避免污染发生
  4. 与普通类相比,可以前向声明,但没有方法

5

有一件事情还没有明确提到——作用域功能为枚举和类方法提供了相同的名称选项。例如:

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

3
因为,正如其他答案中所说,枚举类不能隐式转换为 int / bool,这也有助于避免出现错误的代码,例如:
enum MyEnum {
  Value1,
  Value2,
};
...
if (var == Value1 || Value2) // Should be "var == Value2" no error/warning

3
补充我之前的评论,注意gcc现在有一个警告称为-Wint-in-bool-context,可以捕获这种类型的错误。请注意,此警告旨在检测在布尔上下文中使用整数的情况,以便开发人员能够及时修复这些错误。 - Arnaud

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接