《Effective Modern C++》学习笔记 - Item 10: 倾向于使用有界的枚举(scoped enum)而不是无界的枚举(unscoped enum)

  • 一般在大括号内声明的名称(name,或者严谨点说标识符)的可见范围是限制在大括号内的。然而C++98中的 enum 不同:它内部定义的名称会泄露到定义 enum 自身的区域中。因此也得名 unscoped enum
enum Color { black, white, red };
auto white = false; // VS报错: main::white重定义
  • C++11版本的 scoped enums 通过 enum class 定义,不会导致名称的泄露:
enum class Color { black, white, red };
Color c = white;		// error, 找不到white
Color c = Color::white;	// ok
auto c = Color::white;	// ok
  • scoped enums 的第二个好处:枚举值为强类型,不会被意外地隐式转为整型(甚至接着被转为浮点型)。如果真的想进行转换,用最正确的方法,调用 cast
enum class Color { black, white, red };

Color c = red;			// error
Color c = Color::red;	// ok

if (c < 14.5) {						// 对于enum,隐式转化为double;对于enum class,编译失败
	auto factors = primeFactors(c);	// 对于enum,隐式转化为int;对于enum class,编译失败
}
// 通过static_cast进行显式类型转换,编译通过
if (static_cast<double>(c) < 14.5) {
	auto factors = primeFactors(static_cast<int>(c));
}
  • 还有观点认为 scoped enum 的第三个好处是支持前向声明(forward declaration)unscoped enum 不支持,这在C++11中是错误的。
  • 首先说为什么C++98中 unscoped enum 不支持前向声明,是因为编译器为了内存使用效率,希望找一个能够用于该枚举类型且占用空间尽可能小的底层类型(underlying type)。比如上例中 Color 只有三个枚举值,那么编译器可能就会选择 char,而不是 int 甚至 long。但是枚举内部每个名称的值是可以自定义的,例如:
enum Status {
	good = 0,
	failed = 1,
	incomplete = 100,
	corrupt = 200,
	indeterminate = 0xFFFFFFFF
};
  • 如果看不到枚举的完整定义,编译器就无法决定应该使用什么底层类型,因此C++98只允许枚举定义(definition)而不允许枚举声明(declaration)。但这样的缺点是,例如以上的 State 可能是一个庞大系统很多部分都依赖的类,一旦我们因为某一小部分需要对其做一点修改(例如增加一个状态),都导致整个系统不得不重新编译!这是前向声明能完美解决的问题。

  • C++11的方案是:scoped enum 一个默认的底层类型 int,或者你可以在声明时指出要用的类型以进行覆盖。后者同样适用于 unscoped enum

enum class Status;					// scoped enum前向声明,底层类型为int

enum class Status : std::uint32_t;	// scoped enum前向声明,底层类型为std::uint32_t
enum Color : std::uint8_t;			// unscoped enum前向声明,底层类型为std::uint8_t

// scoped enum指定类型 + 定义
enum class Status : std::uint32_t {
	good = 0,
	failed = 1,
	corrupt = 100
};
  • 注:这里笔者这里发现 unscoped enum 不加底层类型也能在VS中通过编译,从测试结果来看,它也采用int作为默认类型。然而在GCC中相同声明会报编译错误。
    在这里插入图片描述
    在这里插入图片描述

  • 尽管 scoped enum 有着以上优势,作者也提到 unscoped enum 在某些场合非常有用,例如从C++11的 std::tuple 中取元素,利用后者就会使语法简洁清晰:

using UserInfo = std::tuple<std::string,     // 用户名
                            std::string,     // 用户邮箱
                            std::size_t>;    // 声誉值
UserInfo uInfo{ "1", "2", 3 };

auto& val1 = std::get<1>(uInfo);  			// 你真的记得住哪个编号对应哪个成员?

enum UserInfoFieldsUnscoped { uiName, uiEmail, uiReputation };  // 定义辅助的unscoped enum
auto& val2 = std::get<uiEmail>(uInfo);                          // 利用unscoped enum的隐式转换(转换为size_t)

enum class UserInfoFieldsScoped { uiName, uiEmail, uiReputation };                      // 如果换用scoped enum
auto& val3 = std::get<static_cast<std::size_t>(UserInfoFieldsScoped::uiEmail)>(uInfo);  // are you serious??
  • 如果确实要用下面的方法,可以写一个通用的辅助函数来将 scoped enum 自身类型转换为其底层持有的数据类型。方法是使用 std::underlying_type,它可以提取枚举类的底层类型(属于type traits)。根据条款14和15的描述,我们还应该用 constexprnoexcept 来声明它,结果如下:
template<typename E>
constexpr auto toUType(E enumerator) noexcept
{
    return static_cast<std::underlying_type_t<E>>(enumerator);
}

auto& val3 = std::get<toUType(UserInfoFieldsScoped::uiEmail)>(uInfo);	// 简洁了一些, 也许...

总结

  1. C++98风格的 enum 现在被称为 unscoped enum
  2. scoped enum 的枚举值仅在其内部可见。只能通过 cast 将其显式转换为其它类型。
  3. 两种 enum 都支持对底层数据类型的指明。scoped enum 的默认类型为 intunscoped enum 没有默认类型。
  4. scoped enum 总是可以被前向声明,unscoped enum 只能在指明底层类型时被前向声明。(笔者测试结果为,unscoped enum 在MSVC中不指定底层类型也能前向声明,默认采用 int 作为底层类型;GCC中与该描述相符。)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值