【问题标题】:Which Typesafe Enum in C++ Are You Using?您使用的是 C++ 中的哪个类型安全枚举?
【发布时间】:2010-09-18 01:38:28
【问题描述】:

众所周知,C++ 中的内置枚举不是类型安全的。 我想知道那里使用了哪些实现类型安全枚举的类...... 我自己使用以下“自行车”,但它有点冗长和有限:

typesafeenum.h:

struct TypesafeEnum
{
// Construction:
public:
    TypesafeEnum(): id (next_id++), name("") {}
    TypesafeEnum(const std::string& n): id(next_id++), name(n) {}

// Operations:
public:
    bool operator == (const TypesafeEnum& right) const;
    bool operator != (const TypesafeEnum& right) const;
    bool operator < (const TypesafeEnum& right) const;

    std::string to_string() const { return name; }

// Implementation:
private:
    static int next_id;
    int id;
    std::string name;
};

typesafeenum.cpp:

int TypesafeEnum::next_id = 1;

bool TypesafeEnum::operator== (const TypesafeEnum& right) const 
{ return id == right.id; }

bool TypesafeEnum::operator!= (const TypesafeEnum& right) const 
{ return !operator== (right); }

bool TypesafeEnum::operator< (const TypesafeEnum& right) const  
{ return id < right.id; }

用法:

class Dialog 
{
 ...
    struct Result: public TypesafeEnum
    {
        static const Result CANCEL("Cancel");
        static const Result OK("Ok");
    };


    Result doModal();
 ...
};

const Dialog::Result Dialog::Result::OK;
const Dialog::Result Dialog::Result::CANCEL;

补充: 我想我应该对要求更具体。我会尝试总结它们:

优先级 1:将枚举变量设置为无效值应该是不可能的(编译时错误),没有例外。

优先级 2:应该可以通过单个显式函数/方法调用将枚举值转换为 int。

优先级3:尽可能简洁、优雅、方便的声明和使用

优先级 4:将枚举值与字符串相互转换。

优先级 5:(很高兴)可以迭代枚举值。

【问题讨论】:

  • 对不起,你用什么编译器? MSVC 2008 无法编译此示例。这 - static const 结果 CANCEL("Cancel"); - 看起来不像是有效的 C++ 代码...
  • @Stiver:抱歉耽搁了这么久,可能不再相关,但我仍然会回答:这是我的错误,我没有字符串的原始版本,我用螺栓固定了它们发帖前没有检查,对不起。正确的版本会将字符串传递给 cpp 文件中的枚举值的构造函数。
  • 这是目前为止关于枚举和字符串的最佳问题。
  • 我已将标签更改为 c++03,因为显式类型的枚举现在是 C++ 的一部分。请注意,在 C++2003 中已经“类型安全”的枚举。

标签: design-patterns enums enumeration type-safety c++03


【解决方案1】:

我目前正在研究来自Boost Vault(文件名enum_rev4.6.zip)的Boost.Enum 提案。尽管它从未正式提交以包含在 Boost 中,但它可以按原样使用。 (缺乏文档,但由清晰的源代码和良好的测试弥补。)

Boost.Enum 让您可以像这样声明一个枚举:

BOOST_ENUM_VALUES(Level, const char*,
    (Abort)("unrecoverable problem")
    (Error)("recoverable problem")
    (Alert)("unexpected behavior")
    (Info) ("expected behavior")
    (Trace)("normal flow of execution")
    (Debug)("detailed object state listings")
)

并让它自动扩展为:

class Level : public boost::detail::enum_base<Level, string>
{
public:
    enum domain
    {
        Abort,
        Error,
        Alert,
        Info,
        Trace,
        Debug,
    };

    BOOST_STATIC_CONSTANT(index_type, size = 6);

    Level() {}
    Level(domain index) : boost::detail::enum_base<Level, string>(index) {}

    typedef boost::optional<Level> optional;
    static optional get_by_name(const char* str)
    {
        if(strcmp(str, "Abort") == 0) return optional(Abort);
        if(strcmp(str, "Error") == 0) return optional(Error);
        if(strcmp(str, "Alert") == 0) return optional(Alert);
        if(strcmp(str, "Info") == 0) return optional(Info);
        if(strcmp(str, "Trace") == 0) return optional(Trace);
        if(strcmp(str, "Debug") == 0) return optional(Debug);
        return optional();
    }

private:
    friend class boost::detail::enum_base<Level, string>;
    static const char* names(domain index)
    {
        switch(index)
        {
        case Abort: return "Abort";
        case Error: return "Error";
        case Alert: return "Alert";
        case Info: return "Info";
        case Trace: return "Trace";
        case Debug: return "Debug";
        default: return NULL;
        }
    }

    typedef boost::optional<value_type> optional_value;
    static optional_value values(domain index)
    {
        switch(index)
        {
        case Abort: return optional_value("unrecoverable problem");
        case Error: return optional_value("recoverable problem");
        case Alert: return optional_value("unexpected behavior");
        case Info: return optional_value("expected behavior");
        case Trace: return optional_value("normal flow of execution");
        case Debug: return optional_value("detailed object state listings");
        default: return optional_value();
        }
    }
};

它满足您列出的所有五个优先事项。

【讨论】:

  • 为什么要解决一个不存在的问题??!! 头部爆炸
  • 不需要爆头;复杂性很好地隐藏在宏和干净的界面后面。标准 C 枚举存在几个实际问题:缺乏类型安全性、无法查询枚举类型的最大大小、缺乏与字符串的自动转换。
  • 我最初将 boost::enum 设计为一种在编译时构建并且可以在运行时使用的字符串表。最终它变成了一个更通用的类型安全枚举。由于我没有时间专注于文档,因此它从未包含在 boost 中。很高兴在野外看到它!
  • 这看起来非常有用,您仍然应该尝试使用它。也许是使用新的 C++0x 强类型枚举的版本(仍然很高兴获得 boost::optional 接口和字符串转换)。
  • 4.6 版中的BOOST_ENUM 不是完全类型安全的,即涉及运算符==() 时。例如。 BOOST_ENUM(苹果,(a)); BOOST_ENUM(橙色,(o)); bool x = Apple(Apple::a) == Orange::o;编译就好了。原因是 ctor enum_base(index_type index) 不是显式的,并且 index_type 等于 int。不确定其他运营商。
【解决方案2】:

一个不错的折衷方法是这样的:

struct Flintstones {
   enum E {
      Fred,
      Barney,
      Wilma
   };
};

Flintstones::E fred = Flintstones::Fred;
Flintstones::E barney = Flintstones::Barney;

它的类型安全与您的版本不同,但它的用法比标准枚举更好,并且您仍然可以在需要时利用整数转换。

【讨论】:

  • 使用命名空间而不是结构会不会更干净?
  • 有趣的想法。这将消除一个偶尔出现的问题,即有人试图声明 Flintstones 而不是 Flintstones::E。
  • 对命名空间的跟进:对于存在于类或结构中的枚举,您不能使用命名空间。如果枚举存在于类或结构中,我认为使用命名空间更清洁是对的。
  • 另一个解决方案是给结构一个私有构造函数,它可以在嵌套的情况下工作。
  • c++11 枚举类使这完全多余
【解决方案3】:

我使用C++0x typesafe enums。我使用了一些提供 to/from 字符串功能的帮助模板/宏。

enum class Result { Ok, Cancel};

【讨论】:

  • 哪些编译器支持这个?
  • 为什么不问stackoverflow? :-) stackoverflow.com/questions/934183/…
  • @Roddy:所以基本上你使用的功能只有 one 编译器支持,在最新的系统上(与大多数系统相反,没有gcc 4.4)?
  • 我很确定最新的 MSVC 也会支持它。如果它适用于 Linux (GCC)、OS X (GCC) 和 Windows (MSVC),那么对于相当多的用户来说,这就是“足够好”的可移植性。显然不适合所有人;)
  • @Étienne 是的。不幸的是,不再支持彩色和小写字母。
【解决方案4】:

我没有。太多的开销而没有什么好处。此外,能够将枚举转换为不同的数据类型以进行序列化是一个非常方便的工具。我从来没有见过这样的例子,在 C++ 已经提供了足够好的实现的情况下,“类型安全”枚举值得付出开销和复杂性。

【讨论】:

  • 不要射信使...再次投票
  • 可以使用模板实现类型安全的枚举,这样就不会产生任何运行时开销。如果你使用 BOOST_ENUM,所有的复杂性都在一个库中。
【解决方案5】:

我的看法是,您是在发明一个问题,然后将解决方案应用到它上面。我认为没有必要为枚举值做一个详尽的框架。如果您致力于让您的值仅是某个集合的成员,您可以破解一个唯一集合数据类型的变体。

【讨论】:

    【解决方案6】:

    我个人使用的是 typesafe enum idiom 的改编版本。它没有提供您在编辑中陈述的所有五个“要求”,但无论如何我强烈不同意其中的一些。例如,我看不出 Prio#4(将值转换为字符串)与类型安全有什么关系。大多数时候,单个值的字符串表示应该与类型的定义分开(考虑 i18n 的原因很简单)。 Prio#5(迭代,可选)是我希望看到 自然在枚举中发生的最好的事情之一,所以我很难过它在您的请求中显示为“可选”,但是似乎最好通过separate iteration system(例如begin/end 函数或 enum_iterator)来解决,这使得它们可以与 STL 和 C++11 foreach 无缝协作。

    OTOH 这个简单的习语很好地提供了 Prio#3 Prio#1,这要归功于它主要只包装 enums 和更多类型信息。更不用说这是一个非常简单的解决方案,在大多数情况下不需要任何外部依赖标头,因此很容易随身携带。它还具有使枚举范围为 a-la-C++11 的优点:

    // This doesn't compile, and if it did it wouldn't work anyway
    enum colors { salmon, .... };
    enum fishes { salmon, .... };
    
    // This, however, works seamlessly.
    struct colors_def { enum type { salmon, .... }; };
    struct fishes_def { enum type { salmon, .... }; };
    
    typedef typesafe_enum<colors_def> colors;
    typedef typesafe_enum<fishes_def> fishes;
    

    解决方案提供的唯一“漏洞”是它没有解决这样一个事实,即它不会阻止不同类型的 enums(或 enum 和一个 int)被直接比较,因为当你直接使用值你强制隐式转换为int

    if (colors::salmon == fishes::salmon) { .../* Ooops! */... }
    

    但到目前为止,我发现此类问题可以通过简单地提供与编译器的更好比较来解决 - 例如,显式提供一个比较任何两种不同 enum 类型的运算符,然后强制它失败:

    // I'm using backports of C++11 utilities like static_assert and enable_if
    template <typename Enum1, typename Enum2>
    typename enable_if< (is_enum<Enum1>::value && is_enum<Enum2>::value) && (false == is_same<Enum1,Enum2>::value) , bool >
    ::type operator== (Enum1, Enum2) {
        static_assert (false, "Comparing enumerations of different types!");
    }
    

    虽然到目前为止它似乎没有破坏代码,并且它确实可以明确地处理特定问题而不做其他事情,但我不确定这样的事情是“应该" 做(我怀疑它会干扰enums 已经参与在其他地方声明的转换运算符;我很乐意收到有关此的评论)。

    将此与上述类型安全习惯用法相结合,可以在人性化(可读性和可维护性)方面相对接近 C++11 enum class,而无需做任何过于晦涩的事情。而且我不得不承认这样做很有趣,我从来没有想过要询问编译器我是否正在处理enums...

    【讨论】:

    • 在您的第一个代码示例中,您似乎正在使用模板类typesafe_enum - 是否有机会显示该代码?否则很难理解答案(除非我遗漏了一些明显的东西)。
    • 也许我不够明显。它与the typesafe_enum in Wikibooks 基本上是相同的代码(我也在上面链接过)。我添加了几个宏来简化声明并使其与 C++11 兼容。也许我应该把它贴在某个地方。
    【解决方案7】:

    我认为 Java enum 将是一个很好的模型。本质上,Java 表单看起来像这样:

    public enum Result {
        OK("OK"), CANCEL("Cancel");
    
        private final String name;
    
        Result(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    }
    

    Java 方法的有趣之处在于OKCANCELResult 的不可变的单例实例(使用您看到的方法)。您不能再创建Result 的任何实例。由于它们是单例,您可以通过指针/引用进行比较——非常方便。 :-)

    ETA:在 Java 中,不是手动进行位掩码,而是使用EnumSet 来指定位集(它实现了Set 接口,并且像集合一样工作——但使用位掩码实现)。比手写的位掩码操作更具可读性!

    【讨论】:

    • 这很好,但是我们如何在 c++ 中做到这一点呢?而且我认为使用地址进行比较并不是很无害,考虑枚举值是否在磁盘上被序列化?每次我们可能会得到不同的排序。
    • 请记住,在 Java 中,您不能对指针进行 &lt; 比较,只能对 ==!= 进行比较。因此,只要它们不同,就足以通过指针进行比较。 :-) 我会看看我是否可以写一个 C++ 实现。
    • java 枚举似乎是应付奇怪重复模式的一个实例 (en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern)。 naict by googleing,没有人尝试过这种方法。我想知道这是否是由于java泛型和stl语义之间的差异?
    【解决方案8】:

    我对这个here 给出了一个不同主题的答案。这是一种不同风格的方法,它允许大多数相同的功能而无需修改原始枚举定义(因此允许在您未定义枚举的情况下使用)。它还允许运行时范围检查。

    我的方法的缺点是它不会以编程方式强制枚举和辅助类之间的耦合,因此它们必须并行更新。它适用于我,但 YMMV。

    【讨论】:

      【解决方案9】:

      我目前正在https://bitbucket.org/chopsii/typesafe-enums编写我自己的类型安全枚举库

      我不是最有经验的 C++ 开发人员,但由于 BOOST vault 枚举的缺点,我正在写这篇文章。

      请随意查看并自己使用它们,但它们存在一些(希望是轻微的)可用性问题,并且可能根本不是跨平台的。

      如果你愿意,请贡献。这是我的第一个开源项目。

      【讨论】:

        【解决方案10】:

        使用boost::variant

        在尝试了很多上述想法并发现它们缺乏后,我想到了这个简单的方法:

        #include <iostream>
        #include <boost/variant.hpp>
        
        struct A_t {};
        static const A_t A = A_t();
        template <typename T>
        bool isA(const T & x) { if(boost::get<A_t>(&x)) return true; return false; }
        
        struct B_t {};
        static const B_t B = B_t();
        template <typename T>
        bool isB(const T & x) { if(boost::get<B_t>(&x)) return true; return false; }
        
        struct C_t {};
        static const C_t C = C_t();
        template <typename T>
        bool isC(const T & x) { if(boost::get<C_t>(&x)) return true; return false; }
        
        typedef boost::variant<A_t, B_t> AB;
        typedef boost::variant<B_t, C_t> BC;
        
        void ab(const AB & e)
        {
          if(isA(e))
            std::cerr << "A!" << std::endl;
          if(isB(e))
            std::cerr << "B!" << std::endl;
          // ERROR:
          // if(isC(e))
          //   std::cerr << "C!" << std::endl;
        
          // ERROR:
          // if(e == 0)
          //   std::cerr << "B!" << std::endl;
        }
        
        void bc(const BC & e)
        {
          // ERROR:
          // if(isA(e))
          //   std::cerr << "A!" << std::endl;
        
          if(isB(e))
            std::cerr << "B!" << std::endl;
          if(isC(e))
            std::cerr << "C!" << std::endl;
        }
        
        int main() {
          AB a;
          a = A;
          AB b;
          b = B;
          ab(a);
          ab(b);
          ab(A);
          ab(B);
          // ab(C); // ERROR
          // bc(A); // ERROR
          bc(B);
          bc(C);
        }
        

        您可能会想出一个宏来生成样板。 (如果你这样做,请告诉我。)

        与其他方法不同,这种方法实际上是类型安全的,并且适用于旧的 C++。你甚至可以创建像boost::variant&lt;int, A_t, B_t, boost::none&gt; 这样很酷的类型,例如,来表示一个值,可以是 A、B、整数或什么都不是,这几乎是 Haskell98 级别的类型安全。

        需要注意的缺点:

        • 至少在旧的 bo​​ost 上——我在一个 boost 1.33 的系统上——你的变体中限制为 20 个项目;但是有一个解决方法
        • 影响编译时间
        • 疯狂的错误消息——但那是 C++ 给你的

        更新

        为了您的方便,这里是您的类型安全枚举“库”。粘贴此标题:

        #ifndef _TYPESAFE_ENUMS_H
        #define _TYPESAFE_ENUMS_H
        #include <string>
        #include <boost/variant.hpp>
        
        #define ITEM(NAME, VAL) \
        struct NAME##_t { \
          std::string toStr() const { return std::string( #NAME ); } \
          int toInt() const { return VAL; } \
        }; \
        static const NAME##_t NAME = NAME##_t(); \
        template <typename T> \
        bool is##NAME(const T & x) { if(boost::get<NAME##_t>(&x)) return true; return false; } \
        
        
        class toStr_visitor: public boost::static_visitor<std::string> {
        public:
          template<typename T>
          std::string operator()(const T & a) const {
            return a.toStr();
          }
        };
        
        template<BOOST_VARIANT_ENUM_PARAMS(typename T)>
        inline static
        std::string toStr(const boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> & a) {
          return boost::apply_visitor(toStr_visitor(), a);
        }
        
        class toInt_visitor: public boost::static_visitor<int> {
        public:
          template<typename T>
          int operator()(const T & a) const {
            return a.toInt();
          }
        };
        
        template<BOOST_VARIANT_ENUM_PARAMS(typename T)>
        inline static
        int toInt(const boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> & a) {
          return boost::apply_visitor(toInt_visitor(), a);
        }
        
        #define ENUM(...) \
        typedef boost::variant<__VA_ARGS__>
        #endif
        

        并像这样使用它:

        ITEM(A, 0);
        ITEM(B, 1);
        ITEM(C, 2);
        
        ENUM(A_t, B_t) AB;
        ENUM(B_t, C_t) BC;
        

        请注意,您必须在 ENUM 宏中使用 A_t 而不是 A,这会破坏一些魔法。那好吧。另外,请注意现在有一个 toStr 函数和一个 toInt 函数来满足 OP 对字符串和整数的简单转换的要求。我无法弄清楚的要求是一种迭代项目的方法。如果你知道如何写这样的东西,请告诉我。

        【讨论】:

          【解决方案11】:

          不确定这篇文章是否为时已晚,但 GameDev.net 上有一篇文章满足了除第 5 点之外的所有内容(遍历枚举器的能力): http://www.gamedev.net/reference/snippets/features/cppstringizing/

          文章描述的方法允许对现有枚举的字符串转换支持,而无需更改其代码。不过,如果您只想支持新的枚举,我会选择 Boost.Enum(如上所述)。

          【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2015-08-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多