13-6 作用域枚举(枚举类)
尽管在 C++ 中,无作用域枚举是不同的类型,但它们并非类型安全,在某些情况下会导致一些不合理的行为。请考虑以下情况:
#include <iostream>
int main()
{
enum Color
{
red,
blue,
};
enum Fruit
{
banana,
apple,
};
Color color { red };
Fruit fruit { banana };
if (color == fruit) // The compiler will compare color and fruit as integers
std::cout << "color and fruit are equal\n"; // and find they are equal!
else
std::cout << "color and fruit are not equal\n";
return 0;
}
打印出来的内容:


当比较 color 和fruit 时,编译器会先检查它是否知道如何比较 Color和 Fruit。它不知道。接下来,它会尝试将Color 和/或 Fruit为转换整数,看看能否找到匹配项。最终,编译器会确定,如果将两者都转换为整数,就可以进行比较。由于 color 和 fruit 都被设置为枚举器,可以转换为整数值 0 ,因此 color 将等于 fruit。
从语义上讲,这说不通,因为 color 和 fruit 来自不同的枚举,它们本来就不应该可以比较。使用标准枚举器,没有简单的方法可以避免这种情况。
由于这些挑战,以及命名空间污染问题(在全局作用域中定义的非作用域枚举将其枚举器置于全局命名空间中),C++ 设计者认为,更清晰的枚举解决方案将非常有用。
作用域枚举
该解决方案是作用域枚举scoped enumeration(在 C++ 中通常称为枚举类enum class,原因很快就会变得很明显)。
作用域枚举与非作用域枚举类似(13.2 -- 非作用域枚举),但有两个主要区别:它们不会隐式转换为整数,并且枚举器仅放置在枚举的作用域区域中(而不是放置在定义枚举的作用域区域中)。
要创建作用域枚举,我们使用关键字enum class。作用域枚举定义的其余部分与非作用域枚举定义相同。以下是一个示例:
#include <iostream>
int main()
{
enum class Color // "enum class" defines this as a scoped enumeration rather than an unscoped enumeration
{
red, // red is considered part of Color's scope region
blue,
};
enum class Fruit
{
banana, // banana is considered part of Fruit's scope region
apple,
};
Color color { Color::red }; // note: red is not directly accessible, we have to use Color::red
Fruit fruit { Fruit::banana }; // note: banana is not directly accessible, we have to use Fruit::banana
if (color == fruit) // compile error: the compiler doesn't know how to compare different types Color and 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" defines this as a scoped enum rather than an unscoped enum
{
red, // red is considered part of Color's scope region
blue,
};
std::cout << red << '\n'; // compile error: red not defined in this scope region
std::cout << Color::red << '\n'; // compile error: std::cout doesn't know how to print this (will not implicitly convert to int)
Color color { Color::blue }; // okay
return 0;
}
由于作用域枚举为枚举器提供了自己的隐式命名空间,因此无需将作用域枚举放在另一个作用域区域(例如命名空间)内,除非有其他令人信服的理由这样做,因为这样做是多余的。
作用域枚举不会隐式转换为整数。
与非作用域枚举器不同,作用域枚举器不会隐式转换为整数。在大多数情况下,这是一件好事,因为这样做很少有意义,而且有助于防止语义错误,例如比较来自不同枚举的枚举器,或类似这样的表达式red + 5。
请注意,您仍然可以比较同一作用域枚举中的枚举器(因为它们是同一类型):
#include <iostream>
int main()
{
enum class Color
{
red,
blue,
};
Color shirt { Color::red };
if (shirt == Color::red) // this Color to Color comparison is okay
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> // for std::to_underlying() (C++23)
int main()
{
enum class Color
{
red,
blue,
};
Color color { Color::blue };
std::cout << color << '\n'; // won't work, because there's no implicit conversion to int
std::cout << static_cast<int>(color) << '\n'; // explicit conversion to int, will print 1
std::cout << std::to_underlying(color) << '\n'; // convert to underlying type, will print 1 (C++23)
return 0;
}
反之,你也可以将整数静态转换为作用域枚举类型,这在处理用户输入时可能很有用:
#include <iostream>
int main()
{
enum class Pet
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};
std::cout << "Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): ";
int input{};
std::cin >> input; // input an integer
Pet pet{ static_cast<Pet>(input) }; // static_cast our integer to a Pet
return 0;
}
从 C++17 开始,你可以使用整数值对作用域枚举进行列表初始化,无需 static_cast(且与非作用域枚举不同,你无需指定基类):
// using enum class Pet from prior example
Pet pet { 1 }; // okay
最佳实践
除非有充分理由,否则应优先使用作用域枚举而非无作用域枚举。
尽管作用域枚举提供了诸多优势,但无作用域枚举在C++中仍被广泛使用。这是因为某些场景下我们需要隐式转换为int类型(频繁使用static_cast会令人厌烦),且无需额外的命名空间管理。
简化范围枚举器转换为整数的操作(高级)
作用域枚举功能强大,但无法隐式转换为整数有时会成为痛点。若需频繁将作用域枚举转换为整数(例如将作用域枚举项用作数组索引时),每次转换都必须使用static_cast会显著增加代码冗余。
若您需要简化范围枚举类型向整数的转换流程,可通过重载一元运算符+实现便捷转换:
#include <iostream>
#include <type_traits> // for std::underlying_type_t
enum class Animals
{
chicken, // 0
dog, // 1
cat, // 2
elephant, // 3
duck, // 4
snake, // 5
maxAnimals,
};
// Overload the unary + operator to convert an enum to the underlying type
// adapted from https://stackoverflow.com/a/42198760, thanks to Pixelchemist for the idea
// In C++23, you can #include <utility> and return std::to_underlying(a) instead
template <typename T>
constexpr auto operator+(T a) noexcept
{
return static_cast<std::underlying_type_t<T>>(a);
}
int main()
{
std::cout << +Animals::elephant << '\n'; // convert Animals::elephant to an integer using unary operator+
return 0;
}
这将输出:

此方法可防止意外的隐式转换为整数类型,但同时提供了在需要时显式请求此类转换的便捷方式。
使用枚举语句 C++20
C++20 中引入的 using enum 语句将所有枚举器从枚举导入到当前作用域中。当与枚举类类型一起使用时,这允许我们访问枚举类枚举器,而不必在每个枚举器前面加上枚举类的名称前缀。
在某些情况下,这非常有用,例如在switch语句中,它能避免大量重复的前缀:
#include <iostream>
#include <string_view>
enum class Color
{
black,
red,
blue,
};
constexpr std::string_view getColor(Color color)
{
using enum Color; // bring all Color enumerators into current scope (C++20)
// We can now access the enumerators of Color without using a Color:: prefix
switch (color)
{
case black: return "black"; // note: black instead of Color::black
case red: return "red";
case blue: return "blue";
default: return "???";
}
}
int main()
{
Color shirt{ Color::blue };
std::cout << "Your shirt is " << getColor(shirt) << '\n';
return 0;
}

在上面的示例中,Color 是一个枚举类,因此我们通常会使用完全限定名称(例如 Color::blue)来访问枚举器。然而,在函数 getColor() 中,我们添加了使用 enum Color; 的语句,这允许我们在没有 Color:: 前缀的情况下访问这些枚举器。
这使我们不必在 switch 语句中使用多个、冗余、明显的前缀。
问题 #1
定义一个名为 Animal 的枚举类,包含以下动物:猪(pig)、鸡(chicken)、山羊(goat)、猫(cat)、狗(dog)、鸭(duck)。编写一个名为 getAnimalName() 的函数,该函数接受一个 Animal 参数,并使用 switch 语句返回该动物的名称,返回类型为 std::string_view(若使用 C++14 则为 std::string)。另编写函数 printNumberOfLegs(),通过 switch 语句输出每种动物的行走腿数。确保两个函数均包含默认情况,该情况需输出错误信息。在 main() 中分别传入猫和鸡调用 printNumberOfLegs(),输出应如下所示:
A cat has 4 legs.
A chicken has 2 legs.
解决方案:
#include <iostream>
#include <string_view> // C++17
//#include <string> // for C++14
enum class Animal
{
pig,
chicken,
goat,
cat,
dog,
duck,
};
constexpr std::string_view getAnimalName(Animal animal) // C++17
// const std::string getAnimalName(Animal animal) // C++14
{
// If C++20 capable, could use `using enum Animal` here to reduce Animal prefix redundancy
switch (animal)
{
case Animal::chicken:
return "chicken";
case Animal::duck:
return "duck";
case Animal::pig:
return "pig";
case Animal::goat:
return "goat";
case Animal::cat:
return "cat";
case Animal::dog:
return "dog";
default:
return "???";
}
}
void printNumberOfLegs(Animal animal)
{
std::cout << "A " << getAnimalName(animal) << " has ";
// If C++20 capable, could use `using enum Animal` here to reduce Animal prefix redundancy
switch (animal)
{
case Animal::chicken:
case Animal::duck:
std::cout << 2;
break;
case Animal::pig:
case Animal::goat:
case Animal::cat:
case Animal::dog:
std::cout << 4;
break;
default:
std::cout << "???";
break;
}
std::cout << " legs.\n";
}
int main()
{
printNumberOfLegs(Animal::cat);
printNumberOfLegs(Animal::chicken);
return 0;
}


浙公网安备 33010602011771号