5.1.3 强类型枚举以及C++11对原有枚举类型的扩展
非强类型作用域,允许隐式转换为整型,占用存储空间及符号性不确定,都是枚举类的缺点。针对这些缺点,新标准C++11引入了一种新的枚举类型,即“枚举类”,又称“强类型枚举”(strong-typed enum)。
声明强类型枚举非常简单,只需要在enum后加上关键字class。比如:
enum class Type{General,Light,Medium,Heavy};
就声明了一个强类型的枚举Type。强类型枚举具有以下几点优势:
❑强作用域,强类型枚举成员的名称不会被输出到其父作用域空间。
❑转换限制,强类型枚举成员的值不可以与整型隐式地相互转换。
❑可以指定底层类型。强类型枚举默认的底层类型为int,但也可以显式地指定底层类型,具体方法为在枚举名称后面加上“:type”,其中type可以是除wchar_t以外的任何整型。比如:
enum class Type:char{General,Light,Medium,Heavy};
就指定了Type是基于char类型的强类型枚举。
我们可以看看具体的例子,如代码清单5-5所示。
代码清单5-5
include <iostream>
using namespace std;
enum class Type{General,Light,Medium,Heavy};
enum class Category{General=1,Pistol,MachineGun,Cannon};
int main(){
Type t=Type::Light;
t=General;//编译失败,必须使用强类型名称
if(t==Category::General)//编译失败,必须使用Type中的General
cout<<"General Weapon"<<endl;
if(t>Type::General)//通过编译
cout<<"Not General Weapon"<<endl;
if(t>0)//编译失败,无法转换为int类型
cout<<"Not General Weapon"<<endl;
if((int)t>0)//通过编译
cout<<"Not General Weapon"<<endl;
cout<<is_pod<Type>::value<<endl;//1
cout<<is_pod<Category>::value<<endl;//1
return 0;
}
//编译选项:g++ -std=c++11 5-1-5.cpp
在代码清单5-5中,我们定义了两个强类型枚举Type和Category,它们都包含一个称为General的成员。由于强类型枚举成员的名字不会输出到父作用域,因此不会有编译问题。也由于不输出成员名字,所以我们在使用该类型成员的时候必须加上其所属的枚举类型的名字。此外,可以看到,枚举成员间仍然可以进行数值式的比较,但不能够隐式地转为int型。事实上,如果要将强类型枚举转化为其他类型,必须进行显式转换。
事实上,强类型制止enum成员和int之间的转换,使得枚举更加符合“枚举”的本来意义,即对同类进行列举的一个集合,而定义其与数值间的关联则使之能够默认拥有一种对成员排列的机制。而制止成员名字输出则进一步避免了名字空间冲突的问题。这两点跟之前我们使用class对枚举进行封装并无二致。不过新的强类型枚举没有任何class封装枚举的缺点。我们可以看到,Type和Category都是POD类型,不会像class封装版本一样被编译器视为结构体,书写也很简便。在拥有类型安全和强作用域两重优点的情况下,几乎没有任何额外的开销。
此外,由于可以指定底层基于的基本类型,我们可以避免编译器不同而带来的不可移植性。此外,设置较小的基本类型也可以节省内存空间,如代码清单5-6所示。
代码清单5-6
include <iostream>
using namespace std;
enum class C:char{C1=1,C2=2};
enum class D:unsigned int{D1=1,D2=2,Dbig=0xFFFFFFF0U};
int main(){
cout<<sizeof(C::C1)<<endl;//1
cout<<(unsigned int)D::Dbig<<endl;//编译器输出一致,4294967280
cout<<sizeof(D::D1)<<endl;//4
cout<<sizeof(D::Dbig)<<endl;//4
return 0;
}
//编译选项:g++ -std=c++11 5-1-6.cpp
在代码清单5-6中,我们为强类型枚举C指定底层基本类型为char,因为我们只有C1、C2两个值较小的成员,一个char足以保存所有的枚举成员。而对于强类型枚举D,我们指定基本类型为unsigned int,则所有编译器都会使用无符号的unsigned int来保存该枚举。故各个编译器都能保证一致的输出。
相比于原来的枚举,强类型枚举更像是一个属于C++的枚举。但为了配合新的枚举类型,C++11还对原有枚举类型进行了扩展。
首先是底层的基本类型方面。在新标准C++11中,原有枚举类型的底层类型在默认情况下,仍然由编译器来具体指定实现。但也可以跟强类型枚举类一样,显式地由程序员来指定。其指定的方式跟强类型枚举一样,都是枚举名称后面加上“:type”,其中type可以是除wchar_t以外的任何整型。比如:
enum Type:char{General,Light,Medium,Heavy};
在C++11中也是一个合法的enum声明。
第二个扩展则是作用域的。在C++11中,枚举成员的名字除了会自动输出到父作用域,也可以在枚举类型定义的作用域内有效。比如:
enum Type{General,Light,Medium,Heavy};
Type t1=General;
Type t2=Type::General;
General和Type::General两行都是合法的使用形式。
这两个扩展都保留了向后兼容性,也方便了程序员在代码中同时操作两种枚举类型。
此外,我们在声明强类型枚举的时候,也可以使用关键字enum struct。事实上enum struct和enum class在语法上没有任何区别(enum class的成员没有公有私有之分,也不会使用模板来支持泛化的声明)。
有一点比较有趣的是匿名的enum class。由于enum class是强类型作用域的,故匿名的enum class很可能什么都做不了,如代码清单5-7所示。
代码清单5-7
enum class{General,Light,Medium,Heavy}weapon;
int main(){
weapon=General;//无法编译通过
bool b=(weapon==weapon::General);//无法编译通过
return 0;
}
//编译选项:g++ -std=c++11 5-1-7.cpp
代码清单5-7中我们声明了一个匿名的enum class实例weapon,却无法对其设置值或者比较其值(这和匿名struct是不一样的)。事实上,使用enum class的时候,应该总是为enum class提供一个名字(我们实验机上的clang编译器以及XLC编译器甚至会因为用户使用匿名的强类型枚举而阻止编译)。联系到我们在5.1.1中提到的让匿名enum成为“数值的名字”,匿名的enum class则完全做不到。所以在实际使用中必须注意(当然,程序员还是可以通过decltype来获得匿名强类型枚举的类型并且进行使用,即使这样做没什么太大的意义,请参见4.3节)。