关于C++98枚举
在介绍限定作用域的枚举类型(Scoped Enumeration)之前,先来讨论几个关于C++98枚举的问题。
问题一:枚举成员的可见性
一般而言,声明在大括号中的名字仅在该大括号内可见,如果要访问大括号中的名字,需要使用域操作符(::
)。例如要访问类或名称空间中声明的名字,应该这样做:
namespace net
{
const std::string domain = "cynhard.com"; // domain仅在名称空间net中可见
}
class Logger
{
public:
static void Error(const std::string &msg) {} // Error仅在类Logger中可见
};
auto my_domain = net::domain; // ok, 使用域操作符访问名称空间net中的名字domain
//auto my_domain = doman; // 错误!未声明的标识符
Logger::Error("File not found"); // ok, 使用域操作符访问类Logger中的名字Error
//Error("File not found"); // 错误!未声明的标识符
因为domain和Error两个名称都仅在声明它们的大括号内可见,因此无法直接访问它们。但是C++98枚举却没有此限制,C++98枚举中枚举成员(Enumerator)的可见性与枚举类型名称的可见性相同:
// 定义枚举类型LogLevel,可见性为全局可见
// 枚举成员(例如Error)的可见性与LogLevel的可见性相同,即全局可见
enum LogLevel
{
Fatal,
Error,
Warning,
Debug,
};
LogLevel level = Error; // ok, Error全局可见,所以可以直接访问它
//LogLevel level = LogLevel::Error; // C++98: 错误!Error不能以LogLevel限定!
// C++11: 正确!可以通过域操作符访问
// 非限定作用域枚举的成员。(稍后介绍)
可以看到,因为Error的可见性与LogLevel相同,都是全局可见,因此可以直接访问Error,但是不能通过域操作符访问(C++98),也就是说枚举成员的可见性从定义枚举体的大括号内“泄露”(leak)出来了。这样就很容造成名称冲突,假设在同一作用域中有以下声明:
enum LogLevel
{
Fatal,
Error,
Warning,
Debug,
};
enum MessageBoxType
{
//Error, // 错误!Error重定义
Message,
Information,
};
//class Error {}; // 错误! Error重定义
//auto Error = 10; // 错误!Error重定义
从上面代码可以看到,由于LogLevel中声明的Error的可见性“泄露”到了大括号之外,因此造成了名称冲突。这种名称冲突在引入第三方库的情况下更为严重。
由于C++98枚举存在的这种可见性“泄露”现象,C++11将它称为非限定作用域的枚举(Unscoped Enum)。
问题二:弱类型的枚举
C++98枚举类型会隐式转换成整型或浮点型:
enum LogLevel
{
Fatal,
Error,
Warning,
Debug,
};
long factorial(long x)
{
int fact = 1;
for (long i = 2; i <= x; ++i)
{
fact *= i;
}
return fact;
}
LogLevel level = Error;
if (level < 1.2) // level转型为double
{
int x = level + 1; // level转型为int
// ...
}
auto fact = factorial(level); // level转型为long,但是这不是我们的本意,
// 计算level的阶乘毫无意义
// ...
可见C++98 枚举的类型性质很弱,可以隐式转换为整型和浮点型,这很容易造成误用,也违背了C++强类型的设计原则。
问题三:前置声明
C++98枚举的基类型1(underlying type)为整型(char,int等),整型的大小由编译器决定。编译器会根据枚举成员的取值范围确定它的基类型:
// 由于Debug为32位整数,LogLevel的基类型可能为int
enum LogLevel
{
Fatal,
Error,
Warning,
Debug = 0xFFFFFFFF,
};
// 枚举成员的最大值为2,WindowStyle的基类型可能为char
enum WindowStyle
{
Overlapped,
Pop,
Tool,
};
重点在于若编译器不知道枚举成员的取值范围,就不知道如何设置枚举类型的基类型。C++是强类型语言,规定在使用任何类型之前,需要看到这个类型的声明,并且可以根据此声明推断出该类型所占内存的大小。否则无法通过编译。由于C++98枚举类型的大小必须在看到它的定义后才能知道,因此无法实现前置声明(forward declaration):
// interface.h
enum LogLevel; // 前置声明
class Logger
{
// ...
private:
// 打印日志的内部实现
void LogImpl(LogLevel level /*...*/); // 编译错误,编译器不知道LogLevel所需内存大小
};
以上代码编译错误,因为编译器无法知道LogLevel所需要的内存大小。因此必须事先定义枚举,然后再使用:
// interface.h
enum LogLevel // 定义枚举类型
{
Fatal,
Error,
Warning,
Debug,
};
class Logger
{
// ...
private:
// 打印日志的内部实现
void LogImpl(LogLevel level /*...*/); // ok now
};
由此可知,C++98的枚举类型必须定义在头文件中,但是这样做就增加了编译依赖,假设有以下两个源文件都包含了interface.h,其中implementation.cpp实现接口,client.cpp使用接口:
// implementation.cpp
#include "interface.h"
void Logger::LogImpl(LogLevel level /*...*/) { /*...*/ }
//-------------------------------------------------------------------
// client.cpp
#include "interface.h"
int main() { /*...*/ }
那么如果修改LogLevel的定义,比如增加一个枚举成员Info:
// interface.h
enum LogLevel // 定义枚举类型
{
// ...
Info, // 新增加一个成员
};
// ...
就需要将implement.cpp和client.cpp都重新进行编译。但是如果能允许前置声明,就可以将LogLevel的定义放到implement.cpp中,当LogLevel的定义改变时(比如增加或删除枚举成员),只需要重新编译implement.cpp即可。在大型的项目中,合理的利用前置声明来减少编译依赖,可以极大的提高编译效率。
C++11对C++98枚举的改进
通过域操作符限定枚举成员
从问题一的讨论中已经知道,C++98的枚举中枚举成员的可见性与枚举本身的可见性一致,并且不能通过域操作符访问。这样便带来一个问题:
// C++98
enum LogLevel
{
Fatal,
Error = 10,
Warning,
Debug,
};
int main()
{
int Error = 20; // 定义名为Error的int型变量,覆盖掉枚举中的Error
std::cout << Error; // 打印20
//std::cout << LogLevel::Error; // 错误!Error不能以LogLevel限定!
// 那么怎样才能访问到LogLevel的Error呢?
// C++98中没办法。
}
从上面的代码中可以看到,我们没办法访问被覆盖了的LogLevel中的Error。幸运的是,C++11解决了这个问题:
// C++11
enum LogLevel
{
Fatal,
Error = 10,
Warning,
Debug,
};
int main()
{
int Error = 20; // 定义名为Error的int型变量,覆盖枚举中的Error
std::cout << Error; // 打印20
std::cout << LogLevel::Error; // ok now,打印10
}
C++11可以将枚举类型的名字限定在枚举成员名字的前面,即可以通过域操作符限定枚举成员。这样就解决了上面无法访问到被覆盖的枚举成员的问题。另外,为了保持向前兼容,没有限定符的枚举成员仍然支持。
指定枚举的基类型
从问题三的讨论中已经知道,除非编译器看到枚举类型的定义,否则无法确定枚举的基类型,因此也就无法实现前置声明。C++11通过允许指定枚举基类型解决了这一问题,声明的语法为枚举名称后面加冒号(:
),然后再加一个整型类型:
enum LogLevel: int; // 指定基类型
指定了基类型,编译器就知道应该给它分配多少内存了。因此也就可以实现前置声明:
// interface.h
enum LogLevel: int; // 前置声明
class Logger
{
// ...
private:
void LogImpl(LogLevel level /*...*/); // ok,编译器知道分配多少内存给level
};
// ------------------------------------------------
// implementation.cpp
#include "interface.h"
// 枚举的定义放在了实现文件中
enum LogLevel: int // 注意定义中的基类型必须与声明中的一致
{
Fatal,
Error,
Warning,
Debug,
};
void Logger::LogImpl(LogLevel level /*...*/) { /*...*/ }
// ------------------------------------------------
// client.cpp
#include "interface.h"
int main() { /*...*/ }
可以看到将枚举的定义放在了实现文件中,现在修改枚举的定义只需要重新编译implementation.cpp就行了,不再需要重新编译client.cpp。这样就消除了编译依赖。
Scoped Enumeration
虽然C++11对C++98枚举进行了改进,但是C++98枚举仍然存在可见性“泄露”,类型性质不强等问题。于是限定作用域的枚举(Scoped Enumeration)应运而生。定义Scoped Enum需要在enum关键字后面加上class(或者struct)关键字:
enum class LogLevel // 定义限定作用域的枚举
{
Fatal,
Error,
Warning,
Debug,
};
下面来看看限定作用域的枚举相对于非限定作用域的枚举有什么改进。
改进一:限定可见性
Scoped Enum的枚举成员可见性为定义枚举的大括号内:
enum class LogLevel // scoped enum { Fatal, Error, Warning, Debug, }; enum class MessageBoxType { Error, // ok,不与LogLevel::Error冲突 Warning, // ok,不与LogLevel::Warning冲突 }; int Error = 404; // ok,不与LogLevel和MessageBoxType中的Error冲突
可见Scoped Enum的限定性更强,这也是为什么叫它Scoped Enum的原因,而C++98的枚举则称为Unscoped Enum。
只能通过域操作符访问Scoped Enum的成员,即必须通过枚举类型名称限定才能访问枚举成员:
enum class LogLevel // scoped enum { Fatal, Error, Warning, Debug, }; LogLevel level = LogLevel::Error; // ok, 通过枚举类型名称限定枚举成员 //LogLevel level = Error; // 错误!未声明的标识符
改进二:更强的类型
Scoped Enum是更强类型的枚举,不支持隐式转型为其他类型。要转换类型,必须显式地cast。
enum class LogLevel
{
Fatal,
Error,
Warning,
Debug,
};
//int x = LogLevel::Error; 错误,不能将LogLevel赋值给int
int x = static_cast<int>(LogLevel::Error); // ok,显式转型
改进三:基类型
Scoped Enum默认的基类型为int,因此无需指定基类型也可前置声明:
// interface.h
enum class LogLevel; // 前置声明,基类型为int
class Logger
{
// ...
private:
void LogImpl(LogLevel level /*...*/); // ok
};
// ------------------------------------------------
// implementation.cpp
#include "interface.h"
// 枚举的定义放在了实现文件中
enum class LogLevel
{
// ...
};
// ...
// ------------------------------------------------
// client.cpp
#include "interface.h"
int main() { /*...*/ }
当然,也可以显式指定基类型:
// interface.h
enum class LogLevel: char; // 前置声明,基类型为char
// ...
// ------------------------------------------------
// implementation.cpp
// ...
enum class LogLevel: char
{
// ...
};
// ...
Unscoped Enum没有默认基类型,因此想要实现前置声明,必须显式指定基类型。
总结
- Scoped Enum 的成员的可见性为枚举体内。Unscoped Enum 的成员的可见性与定义它的枚举相同。
- Scoped Enum 必须显示转型。Unscoped Enum 可以隐式转型。
- Scoped Enum 基类型默认为int。Unscoped Enum 没有基类型。
- Scoped Enum 和 Unscoped Enum 都可以前置声明。但Unscoped Enum必须指定基类型。
- 这里指可以容纳所有枚举成员的值的整数类型。编译器根据枚举类型的基类型决定枚举对象的大小。 ↩