为什么枚举类优先于普通enum?

我听到一些人推荐在c++中使用enum ,因为他们的类型安全

但这到底是什么意思呢?

426471 次浏览

c++有两种类型的enum:

  1. # EYZ0es
  2. 平原# EYZ0s

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

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

这两者之间有什么区别?

  • enum classes -枚举器名称为枚举的当地的,它们的值为隐式转换为其他类型(如另一个enumint)

  • Plain enums -枚举器名称与枚举及其所在的枚举在同一作用域 值隐式转换为整数和其他类型

例子:

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 classes,因为它们会减少可能导致错误的意外。

与普通枚举相比,使用枚举类的基本优点是,您可以为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??)

从# EYZ0:

enum classes (&;new enums" &;strong enums")解决了三个问题 使用传统的c++枚举:

  • 传统枚举隐式转换为int,当有人不希望枚举充当整数时,会导致错误。
  • 传统枚举将枚举数导出到周围的作用域,从而导致名称冲突。
  • 无法指定enum的底层类型,导致混淆、兼容性问题,并进行正向声明 李不可能。< / >

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

所以,正如其他用户提到的,“strong enums"会让代码更安全。

“经典”的基本类型;enum应该是一个足够大的整数类型,以适合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 charintshort,任何这些类型都足够大,以适应在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?,如果我们可以为有作用域(enum class)和无作用域(enum)枚举选择基础类型,还有什么让enum class成为更好的选择呢?:

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

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

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

c++ 11 FAQ提到以下几点:

传统枚举隐式转换为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
};
  1. 不隐式转换为int
  2. 可以选择哪种类型的底层
  3. ENUM命名空间,避免污染发生
  4. 与普通类相比,可以向前声明,但没有方法

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

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