引子
枚举是C中一个常见的基本类型,一般来说,我们声明一个变量,它的作用域仅存在于它外层花括号包围的范围内,但是这个限制对于枚举(准确说C++98版本的枚举)并不成立:
enum Color { black, white, red }; // black, white, red are in same scope as Color
auto white = false; // error! white already declared in this scope
在C++11中,我们有了新的枚举声明方式来解决这个问题。
正文
在C++98中这种枚举,我们称之为unscoped
枚举。而C++11中我们用scopde
枚举来解决上面的问题:
enum class Color { black, white, red }; // black, white, red are scoped to Color
auto white = false; // fine, no other "white" in scope
Color c = white; // error! no enumerator named "white" is in this scope
Color c = Color::white; // fine
auto c = Color::white; // fine
除了不会名称污染以外,scoped
枚举的第二个好处就是“强类型“,不会进行隐式转换:
enum Color {black,white,red};
Color c = red;
if (c < 14.5) { // error! can't compare Color and double
...
}
而unscoped
枚举则可以隐式转换成整型:
enum Color { black, white, red };
Color c = red;
if (c < 14.5) { // fine
...
}
当然如果非要进行类似的转换,scoped
枚举还是可以用static_cast
转换的。
scoped
枚举的第三个优点就是可以提前声明(forward-declared
),即它们可以在不置顶枚举元素的情况下声明:
enum Color; // error!
enum class Color; // fine
当然准确说造成这种情况的原因是,这种情况下编译器无法确定unscoped
枚举的类型,而此时scoped
枚举是有默认类型的(int类型)。如果我们显示指定它们的类型,则都可以做到提前声明:
enum Color : std::uint8_t;
enum class Color: std::uint32_t ;
提前声明的好处是,如果我们把声明放在头文件,而定义放在cpp文件,那么我们在添加了新的枚举元素的情况下,不需要重新编译大量代码。
当然,unscoped
枚举也有一个好处:把无意义的数值有意义化,如函数返回值,数组下标等,使得代码具有很高的可读性。例如我们用一个std::tuple
保存用户信息:姓名,电子邮件,名声值:
using UserInfo =
std::tuple<std::string, // name
std::string, // email
std::size_t> ; // reputation
UserInfo uInfo;
auto val = std::get<1>(uInfo);
显然上面的代码是正确的,但是可读性并不高,事实上我们很难从最后std::get<1>
中看出得到的结果是用户姓名。但是利用unscoped
枚举和它的隐式转换功能,我们写出可读性极高的代码:
enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
auto val = std::get<uiEmail>(uInfo);
scoped
枚举如果想这么做必须用static_cast
进行强制转换,并不这么优雅。当然我们可以用一个模板函数,std::underlying_type
和constexpr
来帮我们处理这个问题:
template<typename E> // C++11 style
constexpr typename std::underlying_type<E>::type
toUType(E enumerator) noexcept
{
return static_cast<typename
std::underlying_type<E>::type>(enumerator);
}
template<typename E> // C++14 constexpr auto
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}
最后使用时如下:
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
这样好了很多,但是仍然很臃肿,但是这种情况兼顾了防止名称污染的优点。
总结
- C++98中的枚举是无作用域限制的。
- (C++11中的)
scoped
枚举类是有作用域限制的,只能用cast
进行显式类型转换。- 两种枚举类型都支持指定底层的存储类型,对于
scoped
枚举来说默认是int,而unscoped
枚举是未知的,需要编译器进行选择。scoped
枚举总是可以提前声明的,而`unscoped
枚举必须是在明确指定其底层存储的时候才能进行提前声明。