为什么枚举类优先于普通枚举
Why is enum class preferred over plain enum?
我听到一些人建议在C++中使用enum类,因为它们的类型安全性。
但这到底意味着什么?
C++有两种enum
:
enum class
es- 普通
enum
s
以下是几个关于如何声明它们的示例:
enum class Color { red, green, blue }; // enum class
enum Animal { dog, cat, bird, human }; // plain enum
两者之间有什么区别
enum class
es-枚举器名称是枚举的本地,它们的值不会隐式转换为其他类型(如另一个enum
或int
)普通
enum
s-其中枚举器名称与枚举及其在同一范围内值隐式转换为整数和其他类型的
示例:
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
es应该是首选,因为它们引起的意外较少,可能会导致错误。
来自Bjarne Stroustrup的C++11常见问题解答:
enum class
es("新枚举"、"强枚举")解决了三个问题使用传统的C++枚举:
- 传统枚举隐式转换为int,当有人不希望枚举充当整数时会导致错误
- 传统枚举将其枚举器导出到周围的作用域,从而导致名称冲突
- 无法指定
enum
的基础类型,这会导致混淆、兼容性问题,并产生前向声明不可能新的枚举是";enum类";因为它们将传统枚举的方面(名称值)与类的方面(作用域成员和缺少转换)结合在一起。
因此,正如其他用户所提到的;强枚举";会使代码更安全。
";经典的";enum
应为足够大的整数类型,以适合enum
的所有值;这通常是CCD_ 13。此外,每个枚举类型应与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可以指定enum
和enum 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更受欢迎,如果我们可以选择作用域(enum class
)和非作用域(enum
)枚举的底层类型,还有什么能使enum class
成为更好的选择?:
- 它们不会隐式转换为
int
- 它们不会污染周围的命名空间
- 他们可以提前申报
与普通枚举相比,使用枚举类的基本优势在于,您可能对两个不同的枚举具有相同的枚举变量,并且仍然可以解析它们(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??)
枚举用于表示一组整数值。
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++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
。
- 不要隐式转换为int
- 可以选择下面的类型
- ENUM命名空间以避免污染发生
- 与普通类相比,可以向前声明,但没有方法
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;
}
有一件事没有明确提到——scope功能为您提供了一个选项,可以为枚举和类方法使用相同的名称。例如:
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/bool,所以它也有助于避免出现以下错误代码:
enum MyEnum {
Value1,
Value2,
};
...
if (var == Value1 || Value2) // Should be "var == Value2" no error/warning
- 为什么我的开关/机箱在使用枚举时默认?
- 为什么 int 类型的枚举类值不能用作 int
- 为什么static_cast基础类型的枚举类int8_t获得意外值?
- 为什么不能直接引用作用域枚举类成员,而不能为无作用域枚举生成类成员?
- 为什么枚举变量在这里是右值?
- 为什么要在结构中放置枚举,然后使用 typedef 名称?
- 为什么允许将整型、枚举和指向成员的指针类型reinterpret_cast到自身?
- 为什么isocpp允许我们超过枚举类值
- 为什么C++不为枚举类型提供默认"operator>>"函数?
- 为什么 clang 不允许通过实例访问嵌套枚举类?
- 为什么预处理器会用Borland C 识别枚举值,而不是通过视觉C 识别
- 为什么方法重载或枚举标志定义会触发 gcc7.2 编译器警告?
- 如果 int 是"not within the enums range",为什么将 int 转换为强类型枚举会编译?
- 为什么 gcc 没有检测到所有处理的枚举值?
- 为什么可以访问范围之外的枚举器?
- 为什么我必须明确地施放我指定基础类型的枚举
- 为什么可以使用范围运算符和类 A 名称在类 B 中访问类 A(公共)中的枚举
- 受保护的枚举不被视为类型,为什么?
- 为什么将FMTFLAG指定两次 - 作为枚举的一部分,而另一个实例为静态const变量
- 为什么不用static_cast的整数来初始化强大的枚举