本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误。谢谢!

博客已经迁移到这里啦

一般情况下,在花括号中声明一个name(包括变量名,函数名),这个name的可见性会被限制在花括号的作用域内。对于在C++98风格的enum中声明的enum成员却不是这样。这些enum成员的name属于的作用域是enum所在作用域,这意味着在这个作用域中,不能拥有相同的name:

enum Color { black, white, red };	//black,white,red
//和Color在同一个作用域
auto white = false; //错误!white在这个
//作用域已经声明过了

所以事实上,这些enum成员name泄露到enum所在的作用域中去了,这导致官方对于这种enum给出了一个官方术语:unscoped。新的C++11中有一个与此相对应的版本:scoped enum,不会像这样让name泄露:

enum class Color { black, white, red };	//black,white red
//在Color作用域中 auto white = false; //好的,没其他white Color c = white; //错误!在这个作用域中没有
//一个叫“white”的enum成员
Color c = Color::white; //对的 auto c = Color::white; //也是对的(而且和Item 5的建议一样)

因为scoped enum通过“enum class”声明,它们有时候也被叫做enum类。

单是减少命名空间的污染就足够作为理由让我们更偏爱scoped enum了,但是scoped enum还有第二个压倒性的优点:它们的成员属于强类型。unscoped enum的成员能隐式转换到数值类型(然后,从数值类型,可以转换到浮点类型)。因此像下面这样,在语义上是扭曲的代码是完全有效的:

enum Color { black, white, red};		//unscoped enum

std::vector<std::size_t>				//函数,返回x的素因数
primeFactors(std::size_t x); Color c = red;
... if(c < 14.5) { //把Color和double数比较(!) auto factors = //计算Color的素因数(!)
primeFactors(c);
...
}

然而,在”enum“后面添加一个简单的”class“,就能把unscoped enum转变为scoped enum,并且情况会发生很大的改变。这里没有从enum的成员到任何其它类型的隐式转换:

enum class Color { black, white, red };	//scoped

Color c = Color::red;
... if (c < 14.5) { //错误,不能把Color和double
//数进行比较 auto factors = //错误,函数需要一个std::size_t
primeFactors(c); //不能传一个Color进去
...
}

如果你真的想要把Color转换到不同的类型,你需要做的就是:使用cast把Color转换成你需要的类型:

if(static_cast<double>(c) < 14.5) {	//奇怪的代码,但是有效

	auto factors =					//可疑的,但是能通过编译
primeFactors(static_cast<std::size_t>(c));
...
}

比起unscoped enum,scoped enum看起来还有第三个优点,因为scoped enum可以前置声明。也就是,他们的name可以在声明的时候不定义(不明确它们的成员):

enum Color;			//错误

enum class Color;	//对的

这是一个误导。在C++11中,unscoped enum也能前置声明,但是需要做一些额外的工作。这个工作源于一个事实,就是C++中的每个enum都有一个整形的基础类型,这个类型由编译器决定。对于一个unscoped enum,比如Color:

enum Color { black, white, red };

编译器可能选择char来作为基础类型,因为这里只有三个值需要表示。然而,一些enum值的范围会大很多,比如:

enum Status { good = 0,
failed = 1,
incoplete = 100,
corrupt = 200.
indeterminate = 0xFFFFFFFF
};

这里enum值需要表示的范围从0到0xFFFFFFFF。除了一些不寻常的机器(在这些机器中,一个char至少由32bits组成),编译器就必须选择一个大于char的整形类型来表示Status的值。

为了内存的高效利用,只要一个基础类型能成功表示enum中的成员的值的范围,编译器常常会选择这个最小的基础类型,。一些情况下,比起大小,编译器会优先考虑速度,在这种情况下,它们不一定会选择最小的基础类型,但是它们肯定也会在考虑速度的优化后,考虑大小的优化。为了实现这一点,C++98只支持定义(所有的enum成员必须列出来);enum的声明是不被允许的。这样,编译器才能在每个enum使用前,给每个enum选择一个基础类型。

但是“不能前置声明enum”是有缺点的。最需要注意的是,它可能会增加编译依赖性。再次考虑Status enum:

enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};

这个enum可能需要在某个系统中使用,因此它被包含在头文件中,然后系统的每个部分都需要依赖它。如果一个新的status值被添加进来,

enum Status { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};

这样很有可能整个系统都需要重新编译,甚至如果只是一个简单的子系统(更甚一个简单的函数)使用了这个enum。这是被人们所讨厌的。这也是在C++11中,enum前置声明消除的事(编译依赖性)。举个例子,这里有一个scoped enum的声明,它是完美有效的。并且一个函数用它作为一个参数:

enum class Status;					//前置声明

void continueProcessing(Status s);	//使用声明过的enum

如果Status的定义修改了,并且头文件只包含这些声明就不需要重新编译。此外,如果Status修改了(比如,加了一个audited成员),但是continueProcessing的行为没受到影响(比如,因为continueProcessing没有使用audited),continueProcessing的实现也不需要重新编译。

但是如果编译器需要在一个enum使用前知道它的大小,C++11的enum怎么就能使用前置声明而C++98的enum却不行?回答很简单:scoped enum的基础类型总是不变的,并且对于一个unscoped enum,你也能明确它的基础类型。

通常情况下,一个scoped enum的基础类型是int:

enum class Status;					//基础类型是int

如果默认的情况不适合你,你可以自己设置:

enum class Status: std::uint32_t;	//Status的基础类型是
//std::uint32_t

不管怎么样,编译器都知道scoped enum的大小。

为了明确一个unscoped enum的基础类型,你需要做和scoped enum同样的事,这样就能做到前置声明了:

enum Color: std::uint8_t;			//unscoped enum的前置声明
//基础类型是std::uint8_t

基础类型也可以在enum定义的时候明确:

enum class Status: std::uint32_t { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};

基于scoped enum能避免命名空间的污染,不会进行无意义的隐式类型转换的事实,这可能让你感到奇怪:这里起码有一种情况,unscoped enum比起scoped enum会更有用一些。

比如我们在使用C++11的std::tuple的字段时。举个例子,假设对于一个社交网站的用户,我们想设计一个tuple持有name,email地址,reputation值:

using UserInfo =
std::tuple<std::string, //name
std::string, //email
std::size_t> ; //reputation

只通过注释标明每个字段,那么在碰到分离的源文件时,注释将没有作用:

UserInfo uInfo;					//tuple类型的对象
... auto val = std::get<1>(uInfo); //取字段1的值

作为一个程序员,你有很多方式来记录它。你真的能记住字段1代表的是用户的email地址吗?我想不是这样的。使用一个unscoped enum把名字关联到字段中去:

enum UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;
... auto val = std::get<uiEmail>(uInfo); //获得email字段的值

要让这起作用,必须要有从UserInfoFields到std::size_t(std::get需要的类型)的隐式转换。

用scoped enum实现的相应的代码会变得很繁琐:

enum class UserInfoFields { uiName, uiEmail, uiReputation };

UserInfo uInfo;
... auto val =
std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uinfo);

繁琐的情况可以通过写一个函数(传入一个enum成员,返回相应的std::size_t值)来减少,但是这里有点微妙。std::get是一个template,并且你提供的值是template参数(注意使用尖括号,不是圆括号),所以转换函数(enum成员到std::size_t)必须在编译期产生结果。就像Item 15解释的,这意味着它必须是constexpr函数。

事实上,它应该是constexpr函数template,因为它应该能在任何enum下工作。并且如果我们让它继续泛化,我们应该把返回类型也泛化掉。比起返回一个std::size_t,我们需要返回enum的基础类型。通过std::underlying_type能做到这一点。(看Item 9中的type traits信息)最后,我们声明它为noexcept(看Item 14),因为我们知道它不会产生任何异常。最后会产生一个编译期const函数template toUType,它需要一个任意类型的enum成员,并且返回它的值(类型是基础类型):

template<typename E>
constexpr typename std::underlying_type<E>::type
toUType(E enumerator) noexcept
{
return
static_cast<typename
std::underlying_type<E>::type>(enumerator);
}

在C++14中,通过用强大的std::underlying_type_t(看Item 9)来替换typename std::underlying_type::type,toUType能被简化:

template<typename E> 					// C++14
constexpr std::underlying_type_t<E>
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}

在C++14中,更强大的auto返回类型(看Item 3)也同样有效:

template<typename E> 					// C++14
constexpr auto
toUType(E enumerator) noexcept
{
return static_cast<std::underlying_type_t<E>>(enumerator);
}

不管函数怎么写,toUType允许我们这样访问tuple字段:

auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

比起使用unscoped enum,这里还是要写很多东西,但是它也避免了命名空间的污染,和你没有意识到的转换。在很多情况下,比起它的陷阱,你可能觉得多写几个字是合理(但是追溯到很久以前我们的数字通信还在用2400波特的调制解调器时,情况会不一样)。

你要记住的事
  • C++98风格的enum被称为 unscoped enum
  • scoped enum的成员只在enum中可见,它们只有在使用cast时才能转换到其它类型。
  • scoped 和unscopedenum都支持自定义基础类型。scoped enum的默认基础类型是int。unscoped enum没有默认基础类型。
  • scoped enum一直能前置声明,unscoped enum只有在明确基础类型的情况下才能前置声明。

item 10: 比起unscoped enum更偏爱scoped enum的更多相关文章

  1. Item 9: 比起typedef更偏爱别名声明(alias declaration)

    本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 我确信我们都同意使用STL容器是一个好主意,并且我希望在Item ...

  2. item 8: 比起0和NULL更偏爱nullptr

    本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 先让我们看一些概念:字面上的0是一个int,不是一个指针.如果C+ ...

  3. C++11 之 scoped enum

      C++11 枚举类型是“域化的” (scoped enum),相比 C++98 枚举类型的“非域化” (unscoped enum),具有如下优点: 1  命名空间污染  一般来说,声明在花括号内 ...

  4. item 5: 比起显式的类型声明,更偏爱auto

    本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 啊,简单愉快的代码: int x; 等等,讨厌!我忘了初始化x,所 ...

  5. Item 21: 比起直接使用new优先使用std::make_unique和std::make_shared

    本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 让我们先从std::make_unique和std::make_s ...

  6. Item 13: 比起iterator优先使用const_iterator

    本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 STL中的const_iterator等价于pointers-to ...

  7. item 11: 比起private undefined function优先使用deleted function

    本文翻译自modern effective C++,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 如果你为其他开发者提供代码,并且你想阻止他们调用一个特定的函数,你 ...

  8. 细数 C++ 那些比起 C语言 更爽的特性

    结构体定义 C: typedef struct Vertex { int x, y, z; } Vertex; Vertex v1 = { 0 }; // or struct Vertex { int ...

  9. 读书笔记 effective c++ Item 10 让赋值运算符返回指向*this的引用

    一个关于赋值的有趣的事情是你可以将它们链在一起: int x, y, z; x = y = z = ; // chain of assignments 同样有趣的是赋值采用右结合律,所以上面的赋值链被 ...

随机推荐

  1. 如何创建和还原SQL Server 2005数据库?

    在还原SQL Server 2005数据库文件之前,建议先把要还原的数据库文件复制粘贴到某个盘的根目录下,这样便于一会儿找到相关的文件,比如C盘. 先打开SQL Server 2005的Microso ...

  2. 第三章 Hyper-V 2012 R2配置选项

    原书中的第二章 是介绍了下hyper-v的管理器和检查点的使用,导入导出虚拟机,所以我跳过了不高兴写,很简单.直接进入第三部分,介绍虚拟机的三个重要组成部分:CPU,内存,硬盘的配置选项. Hyper ...

  3. 使用 Array2XML把数组转成XML格式,有相同的节点

    最近开发一个项目,需要调用第三方的接口,第三方提供的数据是xml,我直接使用Array2XML把php数组转成XML格式. XML格式如: <root> <body> < ...

  4. Linux : Vim 使用与配置 (附 GitHub 自动化配置脚本)

    由于经常使用 vim 编辑配置文件,有时候也会进行使用vim 编写一些脚本和c/c++ 程序,所以配置一个常用的 vim 是很是必要的.这篇博文主要是记录vim使用和配置相关的一些知识点. 关于vim ...

  5. Python 列表(List)包含的函数与方法

    Python列表函数&方法 Python包含以下函数: 序号 函数 1 cmp(list1, list2)比较两个列表的元素 2 len(list)列表元素个数 3 max(list)返回列表 ...

  6. January 18th, 2018 Week 03rd Thursday

    To strive, to seek, to find, and not to yield. 去奋斗,去寻觅,去探索,但绝不屈服. Strive for our dreams, seek the ve ...

  7. MATLAB三维作图——隐函数

    MATLAB三维作图——隐函数 作者:凯鲁嘎吉 - 博客园http://www.cnblogs.com/kailugaji/ 对于三维隐函数,没有显式表达式,无法通过Matlab现成的3-D画图函数 ...

  8. 卷积运算的本质,以tensorflow中VALID卷积方式为例。

    卷积运算在数学上是做矩阵点积,这样可以调整每个像素上的BGR值或HSV值来形成不同的特征.从代码上看,每次卷积核扫描完一个通道是做了一次四重循环.下面以VALID卷积方式为例进行解释. 下面是pyth ...

  9. Xmind破解

    原始教程 http://df1551e3.wiz03.com/share/s/3v5l7z2wdQVs2llAUc0C_-n_2cPZVe0kEA2n2iw1Ay1ApF_o

  10. Cannot uninstall 'pyserial'. It is a distutils installed project and thus we cannot a ccurately determine which files belong to it which would lead to only a partial uninstall. 解决方法

    最近再升级 pyserial模块时,采用 pip install --upgrade pyserial,待模块下载完成准备卸载原版本时 提示:“Cannot uninstall 'pyserial'. ...