目录
条款10:优先选用限定作用域enum,而非不限定作用域enum(Prefer scoped enum to unscoped enum)
优点分析
优点1(作用域与命名空间):enum class可以避免(外围)名字空间污染。
优点2(隐式转换与类型安全):enum class可以避免隐式类型转换。
优点3(底层类型与前置声明):enum class可以前置声明(降低编译依赖)。
缺点分析
选择之道
条款10:优先选用限定作用域enum,而非不限定作用域enum(Preferscoped enumtounscoped enum)
枚举类型:内置类型,允许定义一组具有特定名称的常量,枚举类型的值在编译时被解析为整数。
- 不限定作用域的枚举类型(unscoped enum):通过enum声明(C++98)
- 普通枚举、传统枚举、弱类型枚举
- 限定作用域的枚举类型(scoped enum):通过enum class或enum struct声明(C++11)
- 枚举类、强类型枚举
枚举类型的值:枚举值 = 枚举量 = 枚举成员
优点分析
优点1(作用域与命名空间):enum class可以避免(外围)名字空间污染。
理论依据:通常而言,如果在“大括号{}”里声明一个名字,则该名字的作用域(可见性)就被限定在“大括号{}”之内。然而,这个规则不适用于C++98风格的枚举类型中定义的枚举量。
概念辨析:枚举类型所在的作用域 = 包含着枚举类型的作用域 = 外围命名空间
enum:没有定义新的作用域,枚举量属于包含着枚举类型的作用域(即枚举量的作用域和枚举类型相同),因此会泄漏(污染)到枚举类型所在的作用域。这意味着,在此作用域内不能含有相同名字的其他实体,否则会导致编译错误或逻辑混淆。
enum class:定义新的作用域,枚举量只属于枚举类型内部的作用域(即枚举量的作用域被限定在枚举类型内),因此不会泄漏(污染)到枚举类型所在的作用域。
优点2(隐式转换与类型安全):enum class可以避免隐式类型转换。
enum:枚举量可以通过隐式类型转换为整数类型(并能够从此处进一步转换为浮点类型),可能引发未预期的行为。(类型不安全、弱类型枚举)
- if条件判断:“枚举量red”可以从“Color(enum枚举类型)”隐式类型转换为“int(整数类型)”,并进一步地转换为“double(浮点类型)”,从而实现将“Color类型”和“double类型”的值进行比较。
- 函数调用:“枚举量red”可以从“Color(enum枚举类型)”隐式类型转换为“std::size_t(整数类型)”,从而实现计算一个“Color类型”的质因数。
- 程序分析:由上述分析可知,尽管下述代码语法合规(能通过编译),但是语义歪曲(不自然)。
enum class:枚举量只能通过强制类型转换(static_cast)为其他类型(不存在任何的隐式转换路径),避免意外的类型转换错误。(类型安全、强类型枚举)
优点3(底层类型与前置声明):enum class可以前置声明(降低编译依赖)。
底层类型(underlying type):所有的枚举类型都有一个整数类型作为其底层类型(或者是默认底层类型;或者是显式指定底层类型;或者是编译器根据定义自动选择)。
前置声明(forward declaration):枚举类型先于枚举量声明。(等价表述,在指定枚举量之前,直接声明枚举类型。)
理论依据(关系):只有当编译器能够确定枚举类型的底层类型时(最终目的是确定其尺寸),才能使用枚举类型。
enum(C++98):由于既没有默认底层类型,也不支持显式指定底层类型,因此不可以前置声明。换言之,C++98仅支持“枚举类型定义”(即列出所有的枚举量,以便编译器根据定义来确定底层类型),不允许“枚举类型声明”。
- 程序分析:对于Color,由于只有三个值需要表示,因此编译器可能选择char作为底层类型。对于Status,由于枚举类型的取值范围较大(例如,此处需要表示从0到0xFFFFFFFF),因此编译器通常会选择比char更大的整数类型作为底层类型(除非是在不常见的机型上,其中char至少包含32bits)。
enum(C++11):虽然没有默认底层类型,但支持显式指定底层类型,因此(条件)可以前置声明(必须显式指定底层类型)。
enum class(C++11):由于既有默认底层类型(int),也支持显式指定底层类型,因此(总是)可以前置声明(无论是否显式指定底层类型)。
问题:前文指出,C++98的枚举类型(即enum)仅支持“枚举类型定义”(即列出所有的枚举量),不允许“枚举类型声明”。请问这种设计(前置声明的缺失)有何缺点?
回答1:在C++98中,枚举(enum)无法前置声明,最大缺点就是增加编译依赖性。考虑enum类型Status,这种枚举很可能会在整个系统中被广泛使用,因此通常会被放在一个头文件中,而系统中的各个部分都会依赖这个头文件。假设后来Status增加了一个新状态值,很可能整个系统都必须重新编译,即使实际上只有一个子系统(甚至可能只有一个函数)使用了这个新的枚举值。
回答2:在C++11中,枚举(enum class和enum)允许前置声明,从而可以消除上述问题。首先,如果修改了Status的定义,则包含下述声明的头文件就不需要重新编译。其次,如果修改了Status的定义(例如,增加枚举值audited),但continueProcessing的行为并没有受到影响(例如,因为continueProcessing根本没有使用audited),则continueProcessing的实现文件也不需要重新编译。
缺点分析
鉴于enum class既能避免命名空间污染,又能避免隐式类型转换,人们很容易认为它在任何情况下都优于enum。然而,实际上至少有一种场景,enum仍然具有优势(并非完全没有用武之地),那就是(在C++11中)使用enum枚举值来表示std::tuple各个字段的索引(给std::tuple各个字段进行命名)。这是因为std::get的模板参数需要整数常量,enum恰好能够隐式转换为std::size_t。
案例分析:假设我们需要为社交网站准备一个std::tuple,用于持有名字、电子邮件和声望值。
实现1(不推荐):
- 程序设计:访问元素时,使用“整数常量”作为std::get的模板类型参数。
- 问题分析:在using别名声明中,注释清楚地指示出tuple各个字段的对应含义。然而,如果在另一个源文件中看到类似“本实现”的代码,这些注释并没有多大帮助。作为程序员,你是否应该记住“第一个字段”代表“用户的电子邮件”吗?(不应该,增加记忆负担)
实现2(推荐):
- 程序设计:访问元素时,使用“enum的枚举值”作为std::get的模板类型参数。
- 优点分析:由于“enum枚举类型”能够隐式类型转换为“std::size_t整数类型”(正是std::get要求的类型),将“字段编号”与“字段名称”关联起来,因此可以避免直接写“魔法数字”,消除记忆需求。
实现3.1(不推荐):
- 程序设计:访问元素时,使用“enum class的枚举值”作为std::get的模板类型参数。
- 问题分析:由于“enum class枚举类型”不能隐式类型转换为其他类型,因此需要(使用static_cast)强制类型转换为“std::size_t整数类型”,代码啰嗦。(若不想那么啰嗦,就得写个函数,见“实现3.2”。)
实现3.2(不推荐):
- 辅助函数(设计思路):传入枚举值,返回其对应的std::size_t值,但这有一点技巧性。
- 首先,由于std::get是函数模板,因此传入的(std::size_t类型)值是模板类型参数(注意此处使用的是<>而不是())。这意味着,(将枚举量由“UserInfoFields”变换为“std::size_t”值的)函数必须在编译期间就计算出结果,因此必须使用constexpr函数(见“条款15”)。
- 其次,由于函数应该能够应用于任何枚举类型,因此(光是constexpr 函数还不够)还得用constexpr函数模板。
- 接着,由于返回值也需要进行泛化,因此(不能返回std::size_t)需要返回枚举类型的底层类型(可通过类型特征std::underlying_type获得,见“条款9”)。
- 最终,由于函数肯定不会产生异常,因此还要将其声明为noexcept(见“条款14”)。
- 辅助函数(具体实现):函数模板toUType接受任意类型的枚举量,并以编译期常量返回其(对应的底层类型的)值。
- 程序设计:访问元素时,使用“enum class的枚举值”作为std::get的模板类型参数。
- 问题分析:即使可以借助辅助函数来简化类型转换代码,使用enum class还是比enum打的字要多。
- 补充说明:enum class可以避免命名空间污染和不经意间地隐式类型转换。在大多数情况下,你应该会觉得多敲几个(几行)字符是合理的代价(是值得的),以避免enum技术带来的陷阱。
选择之道
- enum:需要兼容C语言代码 或 需要与旧系统交互
- enum class:需要更好的作用域控制和类型安全,并且编译器支持C++11及以上版本
enum(传统枚举) | enum class(强类型枚举) | |
作用域 | 没有定义新的作用域 | 定义新的作用域 |
访问方式 | 可以直接访问,无需添加前缀 | 不能直接访问,需要添加作用域 |
维护扩展 | 不易 | 容易 |
类型安全 | 不安全 | 安全 |
打印输出 | 直接cout | 需要进行显式类型转换 |
底层类型 | 允许明确指定 | int |
适用场景 | 需要兼容C语言代码 需要与旧系统交互 | 需要更好的作用域控制和类型安全(前提:编译器需要支持C++11及以上版本) |