[C++] 枚举类型与强枚举类型以及它们的作用域及位运算

陪她去流浪 桃子 2016年11月07日 编辑 阅读次数:8554

C语言中的枚举

C++ 中的枚举类型(enum)是C语言时代就已经存在了的,但是C语言中没有作用域解析运算符(Scope Resolution Operator)“::”的,于是在C语言中,枚举类型的值其实是暴露在全局作用域的,枚举类型的名字只是为了定义该类型的变量所用。

如下所示为 C语言 中枚举类型的使用范例:

enum A
{
    a=1,
    b,
    c,
};

enum B
{
//  c,  // 此处将冲突
    d=10,
    e,
    f,
};

typedef enum {
    g=100,
} C;

int main()
{
    enum A v1 = a;      // 1
    enum B v2 = d;      // 10
    C      v3 = g;      // 100
//  C      v4 = C::g;   // 不支持的语法
}

不同的枚举类型(此处的 A 和 B)却不能具有同名的值,这一特性非常让人难以接受。并且默认暴露在全局作用域内将有可能导致严重的名字冲突(如果 enum 被放在头文件中,被其它很多头文件包含的话)。于是出现了一种简单粗暴的解决办法:给枚举值加上(长长的)前缀:

enum BrowserKernelType
{
    BROWSERKERNELTYPE_IE,
    BROWSERKERNELTYPE_CHROMIUM,
};

定义长长的枚举值名字确实能极大程度上避免名字冲突,但这没有从根本上解决问题,这只是尽量避免。并且,对于我本人来说,我是极其讨厌长长的标识符名字、长长的函数名的,这显得特别冗余。

C++中的枚举

到了 C++ 中,情况有所好转:不像C语言中仅能在结构体内定义(且仅允许定义)POD 类型的数据外,C++ 允许在结构体/类中定义枚举类型(是类型定义,而非变量)。并且,枚举的值仅在其当前所在的闭合范围(enclosing socpe)内可见,闭合范围至少包括:类内、结构体内、命名空间内。

enum E { x, y, z, };

struct A
{
    enum Value { a=1, b=4, };

    enum { c = 10, d, };
};

class B {
public:
    enum E1 { a=1, b=3, };

    enum E2
    {
    //  a,  // 此处将冲突
        c,
        d,
    };

    enum { e, f, };

private:
    enum { g, h, };
};

namespace N
{
    enum E { m=5, n, };
}

int main()
{
    E        v0  = z;            // 2
    A::Value v1  = A::b;         // 4
    A::Value v2  = A::Value::b;  // 4
    int      v3  = A::a;         // 1
//  int      v4  = a;            // 错误,未定义
    int      v5  = B::b;         // 3
    int      v6  = B::d;         // 1
    int      v7  = B::f;         // 1
//  int      v8  = B::g;         // 错误,枚举(值)不可见(私有)
    int      v9  = A::c;         // 10
//  int      v10 = m;            // 错误,未定义
    int      v11 = N::n;         // 6
}

正如前面所述:C++的枚举的值仅在其所在的闭合范围内可见。于是 int v4 = a; 这一行是不被支持的。于是,基于数据隐藏 int v8 = B::g; 也是不被支持的;于是 A::bA::Value::b 其实就是一样的了。

C++11中的枚举

C++基于其闭合范围原则,成功地解决了全局范围内名字冲突的问题,但前提是枚举类型的定义必须是在结构体/类内。对于定义在全局或整个命名空间内的冲突问题依然没有解决。

于是 C++11 再引入了一个新的语法:强类型的枚举(strongly-typed enumeration)。

enum class A { a, b, c, };

// 竟然可以指定基类(底层类型)
enum class B : int
{
    a,
}

int main()
{
//  int v0 = a;         // 错误,未定义
//  int v1 = A::b;      // 错误,无法转换类型:从 A 到 int
    A   v2 = A::b;      // 1
    int v3 = (int)A::c; // 2
    int v4 = B::a;      // 错误,虽然指定了底层类型,但还是不给隐式转换
}

注意上面的类型定义语法:enum class。强类型的枚举类型,至少有两点不同:1) 其值的可见范围为以其类型名的命名空间内;2) 不再允许隐式的类型转换;

不再允许隐式的类型转换(从 枚举 到 整型)意味着,原来能应用在整型类型上的运算符,对于强类型的枚举来说是不可接受的。请继续往下阅读。

枚举作为掩码时的位运算

非强类型的枚举类型允许隐式的转换其类型到整型,于是枚举经常被用作掩码(mask、flags)。

namespace Mask {
enum
{
    a = 1,
    b = 2,
    c = 4,
    d = 8,
};
}

enum class Flags
{
    e = 1,
    f = 2,
    g = 4,
};

int main()
{
    int v1 = Mask::a;               // 1
    int v2 = Mask::b;               // 2
    int v3 = v1 | v2;               // 3
    int v4 = Mask::a | Mask::c;     // 5

    Flags f1 = Flags::e;            // 非直观的 1
    Flags f2 = Flags::f;            // 非直观的 2
//  Flags f3 = f1 | f2;             // 错误,不允许
    int   f4 = (int)f1 | (int)f2;   // 3,强转
//  Flags f5 = f4;                  // 错误,不允许
    Flags f6 = (Flags)10;           // 强转可以
}

本以为解决了命名空间名字冲突的问题,就方便使用了,但却不是这样,对于经常被作为掩码的枚举来说,反而更加麻烦了。

运算符不是可以重载的嘛?于是:

enum class Flags
{
    e = 1,
    f = 2,
    g = 4,
};

Flags operator|(Flags f1, Flags f2)
{
    return (Flags)((int)f1 | (int)f2);
}

Flags& operator|=(Flags& f1, Flags f2)
{
    return f1 =f1 | f2;
}

int main()
{
    Flags f1 = Flags::e;
    Flags f2 = Flags::f;
    Flags f3 = f1 | f2;         // 3

    Flags f4 = f1;              // 1
    f4 |= f2;                   // 3

    Flags f5 = f1;
    f5 |= Flags::g;             // 5
}

靠,真是复杂,位运算有好多啊: &|^&=|=^=~。每个都要这样定义吗?简直疯了。并且枚举类型也非常多。明显不靠谱,就算用模板也麻烦啊。这里有一个更荒谬的:grisumbras/enum-flags: Bit flags for C++11 scoped enums,颇为震惊。作者真是C++玩得6。搞得这么复杂。

推荐的用法

如果确定枚举类型仅是单独使用(没有位运算),那么可以采用强类型的枚举;否则还是使用如下的方式好了。

struct Mask
{
    enum Value
    {
        a = 1,
        b = 2,
        c = 4,
    };
};

int main()
{
    auto mask = Mask::a | Mask::b;

    if(mask & Mask::a) {

    }
    else if(mask & Mask::b) {

    }
}

即解决了名字冲突问题,还能使用位操作。比较优雅的用法。

struct 可以换成 namespace,但是不推荐,因为 namespace 是完全开放读写的。

参考

  1. c++ - enum in a namespace - Stack Overflow
  2. Scope Resolution Operator: ::

标签:C语言 · C++ · 位运算