有作用域的枚举类型(枚举类)

尽管无作用域的枚举类型在C++中是独立的类型,但它们并不是类型安全的,并且在某些情况下,它们会允许你做一些没有意义的事情。考虑以下情况:

#include <iostream>

int main()
{
    enum Color
    {
        red,
        blue,
    };

    enum Fruit
    {
        banana,
        apple,
    };

    Color color { red };
    Fruit fruit { banana };

    if (color == fruit) // 编译器会将 color 和 fruit 作为整数进行比较
        std::cout << "color and fruit are equal\n"; // 并发现它们是相等的!
    else
        std::cout << "color and fruit are not equal\n";

    return 0;
}

该程序的输出为:

color and fruit are equal

当比较 colorfruit 时,编译器会检查它是否知道如何比较一个 Color 和一个 Fruit。它不知道。接下来,它会尝试将 Color 和/或 Fruit 转换为整数,看看是否能找到匹配项。最终,编译器会确定,如果将两者都转换为整数,就可以进行比较。由于 colorfruit 都被设置为转换为整数值 0 的枚举器,因此 color 会等于 fruit

从语义上讲,这是没有意义的,因为 colorfruit 来自不同的枚举,并且不打算进行比较。使用标准枚举器时,没有简单的方法可以防止这种情况。

由于存在这样的挑战,以及命名空间污染问题(在全局作用域中定义的无作用域枚举会将其枚举器放入全局命名空间),C++ 设计者认为需要一个更干净的枚举解决方案。

有作用域的枚举类型

这个解决方案就是有作用域的枚举类型(在C++中通常称为枚举类,原因很快就会明白)。

有作用域的枚举类型的工作方式与无作用域的枚举类型(13.2 – 无作用域的枚举类型)类似,但有两个主要区别:它们不会隐式转换为整数,并且枚举器仅被放置在枚举的作用域区域内(而不是枚举被定义的作用域区域内)。

要创建一个有作用域的枚举类型,我们使用关键字 enum class。其余的有作用域的枚举类型定义与无作用域的枚举类型定义相同。以下是一个示例:

#include <iostream>
int main()
{
    enum class Color // “enum class” 定义这是一个有作用域的枚举类型,而不是无作用域的枚举类型
    {
        red, // red 被视为属于 Color 的作用域区域
        blue,
    };

    enum class Fruit
    {
        banana, // banana 被视为属于 Fruit 的作用域区域
        apple,
    };

    Color color { Color::red }; // 注意:red 不是直接可访问的,我们必须使用 Color::red
    Fruit fruit { Fruit::banana }; // 注意:banana 不是直接可访问的,我们必须使用 Fruit::banana

    if (color == fruit) // 编译错误:编译器不知道如何比较不同类型的 Color 和 Fruit
        std::cout << "color and fruit are equal\n";
    else
        std::cout << "color and fruit are not equal\n";

    return 0;
}

该程序在第 19 行产生编译错误,因为有作用域的枚举类型不会转换为可以与其他类型进行比较的任何类型。

顺便说一下……

class 关键字(连同 static 关键字)是C++语言中最重载的关键字之一,根据上下文可以有不同的含义。尽管有作用域的枚举类型使用了 class 关键字,但它们并不被视为“类类型”(类类型保留用于结构体、类和联合体)。

在这种情况下,enum struct 也可以使用,并且与 enum class 的行为完全相同。然而,使用 enum struct 是非惯用的,因此应避免使用。

有作用域的枚举类型定义自己的作用域区域

与无作用域的枚举类型不同,后者将其枚举器放置在与枚举类型本身相同的作用域中,有作用域的枚举类型仅将其枚举器放置在其枚举的作用域区域内。换句话说,有作用域的枚举类型为其枚举器充当一个命名空间。这种内置的命名空间有助于减少全局命名空间的污染以及在全局作用域中使用有作用域的枚举类型时可能出现的名称冲突。

要访问有作用域的枚举器,我们就像访问与有作用域的枚举类型同名的命名空间中的成员一样进行访问:

#include <iostream>

int main()
{
    enum class Color // “enum class” 定义这是一个有作用域的枚举类型,而不是无作用域的枚举类型
    {
        red, // red 被视为属于 Color 的作用域区域
        blue,
    };

    std::cout << red << '\n';        // 编译错误:在此作用域区域中未定义 red
    std::cout << Color::red << '\n'; // 编译错误:std::cout 不知道如何打印这个(不会隐式转换为整数)

    Color color { Color::blue }; // 好的

    return 0;
}

由于有作用域的枚举类型为其枚举器提供了自己的隐式命名空间,因此除非有其他令人信服的理由,否则没有必要将有作用域的枚举类型放在另一个作用域区域内(例如一个命名空间),因为这样做将是多余的。

有作用域的枚举类型不会隐式转换为整数

与无作用域的枚举器不同,有作用域的枚举器不会隐式转换为整数。在大多数情况下,这是一个好事情,因为这样做很少有意义,并且它有助于防止语义错误,例如比较来自不同枚举类型的枚举器,或者像 red + 5 这样的表达式。

请注意,你仍然可以比较来自同一个有作用域的枚举类型中的枚举器(因为它们是同一种类型):

#include <iostream>
int main()
{
    enum class Color
    {
        red,
        blue,
    };

    Color shirt { Color::red };

    if (shirt == Color::red) // 这种 Color 到 Color 的比较是允许的
        std::cout << "The shirt is red!\n";
    else if (shirt == Color::blue)
        std::cout << "The shirt is blue!\n";

    return 0;
}

偶尔会有一些情况,将有作用域的枚举器视为整数值是有用的。在这些情况下,你可以通过使用 static_cast 显式地将有作用域的枚举器转换为整数。在C++23中,更好的选择是使用 std::to_underlying()(定义在 <utility> 头文件中),它将枚举器转换为枚举类型的底层类型的值。

#include <iostream>
#include <utility> // 用于 std::to_underlying() (C++23)

int main()
{
    enum class Color
    {
        red,
        blue,
    };

    Color color { Color::blue };

    std::cout << color << '\n'; // 不会工作,因为没有隐式转换为整数
    std::cout << static_cast<int>(color) << '\n';   // 显式转换为整数,将打印 1
    std::cout << std::to_underlying(color) << '\n'; // 转换为底层类型,将打印 1 (C++23)

    return 0;
}

相反,你也可以将整数 static_cast 到有作用域的枚举器,这在从用户那里获取输入时很有用:

#include <iostream>

int main()
{
    enum class Pet
    {
        cat, // 赋值为 0
        dog, // 赋值为 1
        pig, // 赋值为 2
        whale, // 赋值为 3
    };

    std::cout << "Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): ";

    int input{};
    std::cin >> input; // 输入一个整数

    Pet pet{ static_cast<Pet>(input) }; // 将整数静态转换为 Pet

    return 0;
}

从C++17开始,你可以使用整数值对有作用域的枚举类型进行列表初始化,而无需 static_cast(与无作用域的枚举类型不同,你不需要指定基数):

// 使用前面示例中的枚举类 Pet
Pet pet { 1 }; // 好的

最佳实践

除非有令人信服的理由,

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"