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

Why is enum class preferred over plain enum?

本文关键字:枚举 为什么 优先于      更新时间:2023-10-16

我听到一些人建议在C++中使用enum,因为它们的类型安全性

但这到底意味着什么?

C++有两种enum:

  1. enum class es
  2. 普通enum s

以下是几个关于如何声明它们的示例:

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

两者之间有什么区别

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

  • 普通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 classes应该是首选,因为它们引起的意外较少,可能会导致错误。

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

enum classes("新枚举"、"强枚举")解决了三个问题使用传统的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 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更受欢迎,如果我们可以选择作用域(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 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

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

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