5.1.2 有缺陷的枚举类型
C/C++的enum有个很“奇怪”的设定,就是具名(有名字)的enum类型的名字,以及enum的成员的名字都是全局可见的。这与C++中具名的namespace、class/struct及union必须通过“名字::成员名”的方式访问相比是格格不入的(namespace等被称为强作用域类型,而enum则是非强作用域类型)。一不小心,程序员就容易遇到问题。比如下面两个枚举:
enum Type{General,Light,Medium,Heavy};
enum Category{General,Pistol,MachineGun,Cannon};
Category中的General和Type中的General都是全局的名字,因此编译会报错。
而在下面的代码清单5-1中,则是一个通过namespace分割了全局空间,但namespace中的成员依然会被enum成员污染的例子。
代码清单5-1
include <iostream>
using namespace std;
namespace T{
enum Type{General,Light,Medium,Heavy};
}
namespace{
enum Category{General=1,Pistol,MachineGun,Cannon};
}
int main(){
T::Type t=T::Light;
if(t==General)//忘记使用namespace
cout<<"General Weapon"<<endl;
return 0;
}
//编译选项:g++5-1-1.cpp
可以看到,Category在一个匿名namespace中,所以所有枚举成员名都默认进入全局名字空间。一旦程序员在检查t的值的时候忘记使用了namespace T,就会导致错误的结果(事实上,有的编译器会在这里做出一些警告,但并不会阻止编译,而有的编译器则不会警告)。
另外,由于C中枚举被设计为常量数值的“别名”的本性,所以枚举的成员总是可以被隐式地转换为整型。很多时候,这也是不安全的。我们可以看看代码清单5-2所示的这个恼人的例子。
代码清单5-2
include <iostream>
using namespace std;
enum Type{General,Light,Medium,Heavy};
//enum Category{General,Pistol,MachineGun,Cannon};//无法编译通过,重复定义了General
enum Category{Pistol,MachineGun,Cannon};
struct Killer{
Killer(Type t,Category c):type(t),category(c){}
Type type;
Category category;
};
int main(){
Killer cool(General,MachineGun);
//…
//…其他很多代码…
//…
if(cool.type>=Pistol)
cout<<"It is not a pistol"<<endl;
//…
cout<<is_pod<Type>::value<<endl;//1
cout<<is_pod<Category>::value<<endl;//1
return 0;
}
//编译选项:g++ -std=c++11 5-1-2.cpp
在上面代码清单5-2的例子中,类型Killer同时拥有Type和Category两种命名类似的枚举类型成员。在一定时候,程序员想查看这位“冷酷”(cool)的“杀手”(Killer)是属于什么Category的。但很明显,程序员错用了成员type。这是由于枚举类型数值在进行数值比较运算时,首先被隐式地提升为int类型数据,然后自由地进行比较运算。当然,程序员的本意并非如此(事实上,我们实验机上的编译器会给出警告说不同枚举类型枚举成员间进行了比较。但程序还是编译通过了,因为标准并不阻止这一点)。
为了解决这一问题,目前程序员一般会对枚举类型进行封装。可以看看代码清单5-2改良后的版本,如代码清单5-3所示。
代码清单5-3
include <iostream>
using namespace std;
class Type{
public:
enum type{general,light,medium,heavy};
type val;
public:
Type(type t):val(t){}
bool operator>=(const Type&t){return val>=t.val;}
static const Type General,Light,Medium,Heavy;
};
const Type Type::General(Type::general);
const Type Type::Light(Type::light);
const Type Type::Medium(Type::medium);
const Type Type::Heavy(Type::heavy);
class Category{
public:
enum category{pistol,machineGun,cannon};
category val;
public:
Category(category c):val(c){}
bool operator>=(const Category&c){return val>=c.val;}
static const Category Pistol,MachineGun,Cannon;
};
const Category Category::Pistol(Category::pistol);
const Category Category::MachineGun(Category::machineGun);
const Category Category::Cannon(Category::cannon);
struct Killer{
Killer(Type t,Category c):type(t),category(c){}
Type type;
Category category;
};
int main(){
//使用类型包装后的enum
Killer notCool(Type::General,Category::MachineGun);
//…
//…其他很多代码…
//…
if(notCool.type>=Type::General)//可以通过编译
cout<<"It is not general"<<endl;
if(notCool.type>=Category::Pistol)//该句无法编译通过
cout<<"It is not a pistol"<<endl;
//…
cout<<is_pod<Type>::value<<endl;//0
cout<<is_pod<Category>::value<<endl;//0
return 0;
}
//编译选项:g++ -std=c++11 5-1-3.cpp
封装的代码长得让人眼花缭乱,不过简单地说,封装即是使得枚举成员成为class的静态成员。由于class中的数据不会被默认转换为整型数据(除非定义相关操作符函数),所以可以避免被隐式转换。而且我们也可以看到,通过封装,枚举的成员也不再会污染全局名字空间了,使用时还必须带上class的名字,这样一来,之前枚举的一些小毛病都能够得到克服。
不过这种解决方案并非完美,至少可能有三个缺点:
❑显然,一般程序员不会为了简单的enum声明做这么复杂的封装。
❑由于封装且采用了静态成员,原本属于POD的enum被封装成为非POD的了(is_pod均返回为0,请对照代码清单5-2所示的情况),这会导致一系列的损失(参见3.6节)。
❑大多数系统的ABI规定,传递参数的时候如果参数是个结构体,就不能使用寄存器来传参(只能放在堆栈上),而相对地,整型可以通过寄存器中传递。所以一旦将class封装版本的枚举作为函数参数传递,就可能带来一定的性能损失。
无论上述哪一条,对于封装方案来说都是极为不利的。
此外,枚举类型所占用的空间大小也是一个“不确定量”。标准规定,C++枚举所基于的“基础类型”是由编译器来具体指定实现的,这会导致枚举类型成员的基本类型的不确定性问题(尤其是符号性)。我们可以看看代码清单5-4所示的这个例子。
代码清单5-4
include <iostream>
using namespace std;
enum C{C1=1,C2=2};
enum D{D1=1,D2=2,Dbig=0xFFFFFFF0U};
enum E{E1=1,E2=2,Ebig=0xFFFFFFFFFLL};
int main(){
cout<<sizeof(C1)<<endl;//4
cout<<Dbig<<endl;//编译器输出不同,g++:4294967280
cout<<sizeof(D1)<<endl;//4
cout<<sizeof(Dbig)<<endl;//4
cout<<Ebig<<endl;//68719476735
cout<<sizeof(E1)<<endl;//8
return 0;
}
//编译选项:g++5-1-4.cpp
在代码清单5-4所示的例子当中,我们可以看到,编译器会根据数据类型的不同对enum应用不同的数据长度。在我们对g++的测试中,普通的枚举使用了4字节的内存,而当需要的时候,会拓展为8字节。此外,对于不同的编译器,上例中Dbig的输出结果将会不同:使用Visual C++编译程序的输出结果为-16,而使用g++来编译输出为4294967280。这是由于Visual C++总是使用无符号类型作为枚举的底层实现,而g++会根据枚举的类型进行变动造成的。