枚举类型的命名空间 - 最佳实践

115

通常,人们需要同时使用几个枚举类型。有时,会遇到名称冲突的问题。有两种解决方案:使用命名空间或使用“更大”的枚举元素名称。但是,命名空间解决方案有两种可能的实现方式:带有嵌套枚举的虚拟类或完整的命名空间。

我正在寻找这三种方法的优缺点。

示例:

// oft seen hand-crafted name clash solution
enum eColors { cRed, cColorBlue, cGreen, cYellow, cColorsEnd };
enum eFeelings { cAngry, cFeelingBlue, cHappy, cFeelingsEnd };
void setPenColor( const eColors c ) {
    switch (c) {
        default: assert(false);
        break; case cRed: //...
        break; case cColorBlue: //...
        //...
    }
 }


// (ab)using a class as a namespace
class Colors { enum e { cRed, cBlue, cGreen, cYellow, cEnd }; };
class Feelings { enum e { cAngry, cBlue, cHappy, cEnd }; };
void setPenColor( const Colors::e c ) {
    switch (c) {
        default: assert(false);
        break; case Colors::cRed: //...
        break; case Colors::cBlue: //...
        //...
    }
 }


 // a real namespace?
 namespace Colors { enum e { cRed, cBlue, cGreen, cYellow, cEnd }; };
 namespace Feelings { enum e { cAngry, cBlue, cHappy, cEnd }; };
 void setPenColor( const Colors::e c ) {
    switch (c) {
        default: assert(false);
        break; case Colors::cRed: //...
        break; case Colors::cBlue: //...
        //...
    }
  }

19
首先,我会使用Color::Red(红色)表示情感Feeling:Angry(愤怒),等等。 - abatishchev
好问题,我使用了命名空间方法.... ;) - MiniScalope
19
“c”前缀会降低可读性。 - User
5
注意,你不需要像这样命名枚举 enum e {...},枚举可以是匿名的,即 enum {...},当包装在命名空间或类中时更有意义。 - kralyk
例如:enum FOO{}; void bar(FOO e); 但如果我们有 enum{} void bar2(???); 那么它的类型是什么? - aCuria
@kralyk:你说得对,但只适用于类。对于命名空间,最好保留枚举的非匿名性,因为您不能将命名空间的名称本身用作类型名称;您只能使用该内部枚举的名称。 - SasQ
8个回答

86

原始的C++03答案:

namespace(而不是class)的好处是你可以在需要时使用using声明。

使用namespace的问题在于命名空间可能在代码的其他地方被扩展。在一个大型项目中,您无法保证两个不同的枚举类型都不会认为它们叫做eFeelings

为了让代码看起来更简单,我使用struct,因为您可能希望内容是公开的。

如果您正在执行这些实践,则已经领先并且可能不需要进一步审查此内容。

更新的C++11建议:

如果您使用的是C++11或更高版本,enum class将隐式地为枚举值限定其名称。

使用enum class,您将失去对整数类型的隐式转换和比较,但实际上这可能有助于发现模棱两可或错误的代码。


4
我赞同结构思想。谢谢夸奖 :) - xtofl
3
+1 我记不得C++11中"enum class"的语法。没有这个特性,枚举是不完整的。 - Grault
是否有使用“using”来隐式定义“enum class”的作用域的选项? 例如,将'using Color::e;'添加到代码中是否允许使用'cRed'并知道这应该是Color :: e :: cRed? - JVApen

22

顺便提一下,C++0x中有一种新的语法可以处理你提到的情况(详见C++0x维基页面)。

enum class eColors { ... };
enum class eFeelings { ... };

14

我将之前的答案进行了混合,得到了以下结果:(编辑注明:这仅适用于C++11之前。如果您正在使用C++11,请使用enum class

我有一个包含所有项目枚举的大型头文件,因为这些枚举在工作类之间共享,将这些枚举放在工作类本身中是没有意义的。

struct 避免使用 public: 语法糖,而 typedef 允许您在其他工作类中实际声明这些枚举的变量。

我认为使用命名空间并没有什么帮助。也许这是因为我是一名 C# 程序员,在那里当您引用值时必须使用枚举类型名称,所以我习惯了这种方式。

    struct KeySource {
        typedef enum { 
            None, 
            Efuse, 
            Bbram
        } Type;
    };

    struct Checksum {
        typedef enum {
            None =0,
            MD5 = 1,
            SHA1 = 2,
            SHA2 = 3
        } Type;
    };

    struct Encryption {
        typedef enum {
            Undetermined,
            None,
            AES
        } Type;
    };

    struct File {
        typedef enum {
            Unknown = 0,
            MCS,
            MEM,
            BIN,
            HEX
        } Type;
    };

...

class Worker {
    File::Type fileType;
    void DoIt() {
       switch(fileType) {
       case File::MCS: ... ;
       case File::MEM: ... ;
       case File::HEX: ... ;
    }
}

10

我肯定会避免使用类来实现这个功能,而是使用命名空间。问题归结为是使用命名空间还是为枚举值使用唯一的id。个人而言,我会使用命名空间,这样我的id可以更短,也更容易理解。然后应用程序代码可以使用 'using namespace' 指令,使得一切更加可读。

从你上面的例子中:

using namespace Colors;

void setPenColor( const e c ) {
    switch (c) {
        default: assert(false);
        break; case cRed: //...
        break; case cBlue: //...
        //...
    }
}

你能否给出一些提示,为什么你会更喜欢使用命名空间而不是类? - xtofl
@xtofl:你不能写成“使用类Colors”。 - MSalters
2
@MSalters: 你也不能写Colors someColor = Red;,因为命名空间并不构成一种类型。相反,你必须写成Colors::e someColor = Red;,这是相当反直觉的。 - SasQ
@SasQ 如果你想在 switch 语句中使用一个 struct/class,你不是也得使用 Colors::e someColor 吗?如果你使用匿名的 enum,那么 switch 就无法评估一个 struct - Macbeth's Enigma
1
抱歉,但是 const e c 对我来说几乎不可读 :-) 不要那样做。然而,使用命名空间是可以的。 - dhaumann

8
使用类的优点在于您可以在其基础上构建一个完整的类。
#include <cassert>

class Color
{
public:
    typedef enum
    {
        Red,
        Blue,
        Green,
        Yellow
    } enum_type;

private:
    enum_type _val;

public:
    Color(enum_type val = Blue)
        : _val(val)
    {
        assert(val <= Yellow);
    }

    operator enum_type() const
    {
        return _val;
    }
};

void SetPenColor(const Color c)
{
    switch (c)
    {
        case Color::Red:
            // ...
            break;
    }
}

如上例所示,通过使用类,您可以实现以下功能:
  1. 禁止(遗憾的是,不是编译时)C++从无效值进行转换,
  2. 为新创建的枚举设置一个(非零)默认值,
  3. 添加进一步的方法,例如返回选择的字符串表示。
请注意,您需要声明operator enum_type(),以便C++知道如何将您的类转换为基础枚举。否则,您将无法将类型传递给switch语句。

1
这个解决方案与此处显示的内容有关吗?:https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Type_Safe_Enum 我在考虑如何将其制作为一个模板,这样每次需要使用它时都不必重写此模式。 - SasQ
@SasQ:看起来很相似,是的。那可能是同样的想法。然而,除非您在其中添加了许多“常见”方法,否则我不确定模板是否有益。 - Michał Górny
并不完全正确。您可以通过 const_expr 或 int 的 private 构造函数进行编译时检查枚举是否有效。 - xryl669

7
使用类或命名空间的区别在于,类不能像命名空间那样重新打开。这避免了命名空间在未来可能被滥用的可能性,但也存在一个问题,即您无法添加到枚举集合中。
使用类的一个可能好处是,它们可以用作模板类型参数,而命名空间则不行。
class Colors {
public:
  enum TYPE {
    Red,
    Green,
    Blue
  };
};

template <typename T> void foo (T t) {
  typedef typename T::TYPE EnumType;
  // ...
}

个人而言,我不太喜欢使用缩写,更喜欢全称,所以我并不认为这是命名空间的优点。但是,在你的项目中,这可能不是最重要的决定!


不重新打开类也是一个潜在的缺点。颜色列表也不是有限的。 - MSalters
1
我认为不重新打开一个类是一个潜在的优势。如果我想要更多的颜色,我只需要用更多的颜色重新编译这个类。如果我不能这样做(比如说我没有代码),那么无论如何我都不想碰它。 - Thomas Eding
@MSalters:无法重新打开一个类不仅不是缺点,而且是一种安全工具。因为如果能够重新打开一个类并向枚举中添加一些值,则可能会破坏已经依赖该枚举并仅了解旧值集的其他库代码。它将很高兴地接受这些新值,但在运行时由于不知道如何处理它们而崩溃。请记住开闭原则:类应该关闭以进行修改,但应该开放以进行扩展。通过扩展,我指的不是向现有代码中添加内容,而是用新代码(例如派生)包装它。 - SasQ
所以,当你想要扩展枚举时,你应该将它作为一个新类型派生自第一个枚举(如果在C++中容易实现的话... ;/)。然后,新代码可以安全地使用这些新值,但旧代码只能接受旧值(通过转换)而不会接受任何来自这些新值的错误类型(扩展类型)。只有旧值被理解为正确(基本)类型的值才会被接受(也偶然成为新类型的值,因此也可以被新代码接受)。 - SasQ

5
由于枚举被限定在它们所在的作用域中,最好将它们包装在某个东西中,以避免污染全局命名空间并帮助避免名称冲突。我更喜欢使用命名空间而不是类,因为namespace感觉像一个物品袋,而class则感觉像一个强大的对象(与struct vs. class的辩论相对比)。命名空间的一个可能好处是它可以稍后扩展 - 如果您正在处理无法修改的第三方代码,则非常有用。
当然,当我们使用C++0x时,这一切都毫无意义了。

枚举类...我需要查一下! - xtofl

3

我也倾向于将枚举包裹在类中。

正如Richard Corden所提到的,类的好处在于它是C++中的类型,因此可以在模板中使用。

我有一个专门的toolbox::Enum类来满足我的需求,我会针对每个模板进行特化,提供基本函数(主要是将枚举值映射到std::string上,以便更容易地进行I/O读写)。

我的小模板还具有检查允许值的附加优势。编译器在检查值是否真正位于枚举中时有些松散:

typedef enum { False: 0, True: 2 } boolean;
   // The classic enum you don't want to see around your code ;)

int main(int argc, char* argv[])
{
  boolean x = static_cast<boolean>(1);
  return (x == False || x == True) ? 0 : 1;
} // main

我一直觉得编译器不能捕获这个问题,因为你最终得到的枚举值没有意义(而且你也不会预料到这种情况)。

同样的问题也存在:

typedef enum { Zero: 0, One: 1, Two: 2 } example;

int main(int argc, char* argv[])
{
  example y = static_cast<example>(3);
  return (y == Zero || y == One || y == Two) ? 0 : 1;
} // main

再次,main函数将返回错误。
问题在于编译器会将enum适配到最小的可用表示形式(这里需要2位),并且所有适配到该表示形式的内容均被视为有效值。
此外,有时你更愿意使用循环枚举可能的值而不是switch语句,这样当你向enum中添加一个值时,你不必修改所有的switch语句。
总的来说,我的小工具真的让我的枚举变得更简便了(当然,它增加了一些开销),只有因为我将每个枚举嵌套在它自己的结构体中,才有可能实现这一点 :)

4
有趣。您介意分享一下您的枚举类的定义吗? - momeara

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接