C++ 枚举类型的思考

至从C语言开始enum类型就被作为用户自定义分类有限集合常量的方法被引入到了语言当中,而且一度成为C++中定义编译期常量的唯一方法(后来在类中引入了静态整型常量)。
根据上面对enum类型的描述,有以下几个问题:
1.到底enum所定义出来的类型是一个什么样的类型呢?
2.作为一个用户自定义的类型其所占用的内存空间是多少呢?
3.使用enum类型是否真的能够起到有限集合常量的边界约束呢?
4.大家可能都知道enum类型和int类型具有隐示(自动)转换的规则,那么是否真的在任何地方都可以使用enum类型的变量来代替int类型的变量呢?

 1. 到底enum所定义出来的类型是一个什么样的类型呢?
 在C++中大家都知道仅仅有两种大的类型分类:POD类型(注(1))和类类型。
 enum所定义的类型其实属于POD类型,也就是说它会参与到POD类型的隐示转换规则当中去,所以才会出现enum类型与int类型之间的隐示转换现象。
 那么也就是说enum所定义的类型不具备名字空间限定能力(因为不属于类类型),其所定义的常量子具备和enum类型所在名字空间相同的可见性,由于自身没有名字限定能力,所以会出现名字冲突现象。
 如:
           struct CEType
           {
               enum EType1 { e1, e2 };
               enum EType2 { e1, e2 };
           };
       上面的例子会出现e1、e2名字冲突编译时错误,原因就在于枚举子(e1、e2)是CEType名字空间中的名字,同样在引用该CEType中的枚举子时必须采用CEType::e1这样的方式进行,而不是CEType::EType1::e1来进行引用。

    注(1)POD类型:
 你可以将 POD 类型看作是一种来自外太空的用绿色保护层包装的数据类型,POD 意为“Plain Old Data”(译者:如果一定要译成中文,那就叫“彻头彻尾的老数据”怎么样!)这就是 POD 类型的含义。
 其确切定义相当粗糙(参见 C++ ISO 标准),其基本意思是 POD 类型包含与 C 兼容的原始数据。
 例如,结构和整型是 POD 类型,但带有构造函数或虚拟函数的类则不是。
 POD 类型没有虚拟函数,基类,用户定义的构造函数,拷贝构造,赋值操作符或析构函数。
   为了将 POD 类型概念化,你可以通过拷贝其比特来拷贝它们。此外, POD 类型可以是非初始化的。

 2. 作为一个用户自定义的类型其所占用的内存空间是多少呢?
       该问题就是sizeof( EType1 )等于多少的问题,是不是每一个用户自定义的枚举类型都具有相同的尺寸呢?
 在大多数的32位编译器下(如:VC++、gcc等)一个枚举类型的尺寸其实就是一个sizeof( int )的大小,难道枚举类型的尺寸真的就应该是int类型的尺寸吗?
 其实不是这样的,在C++标准文档(ISO14882)中并没有这样来定义,
 标准中是这样说明的:“枚举类型的尺寸是以能够容纳最大枚举子的值的整数的尺寸”,
 同时标准中也说名了:“枚举类型中的枚举子的值必须要能够用一个int类型表述”,
 也就是说,枚举类型的尺寸不能够超过int类型的尺寸,但是是不是必须和int类型具有相同的尺寸呢?
 上面的标准已经说得很清楚了,只要能够容纳最大的枚举子的值的整数就可以了,那么就是说可以是char、short和int。
 例如:
           enum EType1 { e1 = CHAR_MAX };
           enum EType2 { e2 = SHRT_MAX };
           enum EType3 { e3 = INT_MAX  };
       上面的三个枚举类型分别可以用char、short、int的内存空间进行表示,也就是:
           sizeof( EType1 ) == sizeof( char  );
           sizeof( EType2 ) == sizeof( short );
           sizeof( EType3 ) == sizeof( int   );
       那为什么在32位的编译器下都会将上面三个枚举类型的尺寸编译成int类型的尺寸呢?
       主要是从32位数据内存对其方面的要求进行考虑的,在某些计算机硬件环境下具有对齐的强制性要求(如:sun SPARC),
       有些则是因为采用一个完整的32位字长CPU处理效率非常高的原因(如:IA32)。
       所以不可以简单的假设枚举类型的尺寸就是int类型的尺寸,说不定会遇到一个编译器为了节约内存而采用上面的处理策略。
    3. 使用enum类型是否真的能够起到有限集合常量的边界约束呢?
       首先看一下下面这个例子:
           enum EType { e1 = 0, e2 };
           void func1( EType e )
           {
               if ( e == e1 )
               {
                   // do something
               }
               // do something because e != e1 must e == e2
           }
           void func2( EType e )
           {
               if ( e == e1 )
               {
                   // do something
               }
               else if ( e == e2 )
               {
                   // do something
               }
           }
          
           func1( static_cast<EType>( 2  ) );
           func2( static_cast<EType>( -1 ) );
       上面的代码应该很清楚的说明了这样一种异常的情况了,在使用一个操出范围的整型值调用func1函数时会导致函数采取不该采取的行为,而第二个函数可能会好一些他仅仅是忽略了超出范围的值。
       这就说明枚举所定义的类型并不是一个真正强类型的有限常量集合,这样一种条件下和将上述的两个函数参数声明成为整数类型没有任何差异。所以以后要注意标准定义中枚举类型的陷阱。
      (其实只有类类型才是真正的强类型)
      
    4. 是否真的在任何地方都可以使用enum类型的变量来代替int类型的变量呢?
       通过上面的讨论,其实枚举类型的变量和整型变量具有了太多的一致性和可互换性,那么是不是在每一个可以使用int类型的地方都可以很好的用枚举类型来替代呢?
       其实也不是这样的,毕竟枚举类型是一个在编译时可区分的类型,
       同时第2点的分析枚举类型不一定和int类型具有相同的尺寸,这两个差异就决定了在某些场合是不可以使用枚举类型来代替int类型的。
       如:
           第一种情况:
               enum EType { e1 = 0, e2, e3 };
               EType val;
               std::cin >> val;
           第二种情况:
               enum EType { e1 = 0, e2, e3 };
               EType val;
               std::scanf( "%d", &val );
       上面的两种情况看是基本上属于同一种类型的问题,其实不然。第一种情况会导致编译时错误,
       会因为std::cin没有定义对应的枚举类型的重载>>运算符而出错,这就说明枚举类型是一种独立和鉴别的类型;
       而第二种情况不会有任何编译时问题,但是可能会导致scanf函数栈被破坏而使得程序运行非法,为什么会这样呢?
       上面已经分析过了枚举类型变量的尺寸不一定和int类型相同,这样一来我们采用%d就是说将枚举类型变量val当作4字节的int变量来看待并进行参数压栈,
       而在某些编译器下sizeof( val )等于1字节,这样scanf函数就会将val变量地址中的后续的三字节地址也压入栈中,
       并对其进行赋值,也许val变量后续的三个字节的地址没有特殊含义可以被改写(比如是字节对齐的空地址空间),
       可能会认为他不会出现错误,其实不然,在scanf函数调用结束后会进行栈清理,
       这样一来会导致scanf函数清理了过多的地址空间,从而破坏了外围函数的栈指针的指向,从而必然会导致程序运行时错误。

由上面的说明枚举类型有那么多的缺点,那我们怎样才能够有一个类型安全的枚举类型呢?实际上,在最新的 C++0x 标准草案中有关于枚举作用域问题的提案,但最终的解决方案会是怎样的就无法未卜先知了,毕竟对于象 C++ 这样使用广泛的语言来说,任何特性的增删和修改都必须十分小心谨慎。

当然,我们可以使用一些迂回的方法来解决这个问题(C++ 总是能给我们很多惊喜和意外)。

例如,我们可以把枚举值放在一个结构里,并使用运算符重载来逼近枚举的特性:

struct FileAccess {
    enum __Enum {
        Read = 0x1,
        Write = 0x2
    };
    __Enum _value; // 枚举值

    FileAccess(int value = 0) : _value((__Enum)value) {}
    FileAccess& operator=(int value) {
        this->_value = (__Enum)value;
        return *this;
    }
    operator int() const {
        return this->_value;
    }
};

我们现在可以按照希望的方式使用这个枚举类型:

FileAccess access = FileAccess::Read;

并且,因为我们提供了到 int 类型的转换运算符,因此在需要 int 的地方都可以使用它,例如 switch 语句:

switch (access) {
    case FileAccess::Read:
        break;
    case FileAccess::Write:
        break;
}

当然我们不愿意每次都手工编写这样的结构。通过使用宏,我们可以很容易做到这一点:

#define DECLARE_ENUM(E) \
struct E \
{ \
public: \
    E(int value = 0) : _value((__Enum)value) { \
    } \
    E& operator=(int value) { \
        this->_value = (__Enum)value; \
        return *this; \
    } \
    operator int() const { \
        return this->_value; \
    } \
\
    enum __Enum {

#define END_ENUM() \
    }; \
\
private: \
    __Enum _value; \
};

我们现在可以按如下的方式定义前面的枚举,并且不比直接写 enum 复杂多少。

DECLARE_ENUM(FileAccess)
    Read = 0x1,
    Write = 0x2,
END_ENUM()

DECLARE_ENUM(FileShare)
    Read = 0x1,
    Write = 0x2,
END_ENUM()

posted on 2009-03-23 18:16 Randy 阅读(6257) 评论(7)  编辑 收藏 引用

评论

# re: C++ 枚举类型的思考 2009-09-24 15:14 gong.max

感谢,学习了  回复  更多评论   

# re: C++ 枚举类型的思考 2010-05-12 23:04 water

那个operator int()根本就是蛇足,完全没有用。
这段代码:
switch (access) {
case FileAccess::Read:
break;
case FileAccess::Write:
break;
}
之所以正确,和那个operator int()完全没有关系。从语法上分析,FileAccess::Read并不是FileAccess类型,而是FileAccess::__Enum类型的,在这里,因为enum是一个完全可以被int容纳的类型,就如你所说“枚举类型中的枚举子的值必须要能够用一个int类型表述”,所以自动转换了。
迄今为止,包括msvc10,g++4.4,intel C++11在内的编译器,把enum当成int用时,都不需要显式的转换。  回复  更多评论   

# re: C++ 枚举类型的思考[未登录] 2011-10-15 22:17 lin

在scanf函数调用结束后会进行栈清理,
这样一来会导致scanf函数清理了过多的地址空间

能不能详细的介绍下?不是很明白..thanks  回复  更多评论   

# re: C++ 枚举类型的思考[未登录] 2011-10-16 20:10 lin

按照上面的说法,传递进来四个字节(int占用4B)地址空间,scanf调用结束清栈的时候,不是也清除这四个字节的地址空间吗?怎么会多清除呢?呃,解释下好吗,谢谢  回复  更多评论   

# re: C++ 枚举类型的思考[未登录] 2012-08-07 19:01 春秋十二月

不错,但从整型到枚举的转换,需注意是否越界  回复  更多评论   

# re: C++ 枚举类型的思考[未登录] 2012-10-22 03:43 K

@water
operator int()还是需要的,在 switch (access) 中,access 是 FileAccess 类型,而 FileAccess 没有转换到 FileAccess::__Enum 或 int 的定义,所以如果没有operator int(),编译是通不过的。  回复  更多评论   

# re: C++ 枚举类型的思考[未登录] 2014-04-25 22:28 kk

operator int()还是需要的
支持
这个很需要的
现在C++11 出来了,域的问题解决了
但还是需要 operator int()  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2012年8月>
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

导航

统计

常用链接

留言簿(3)

随笔档案

搜索

最新评论

阅读排行榜

评论排行榜