代码之家  ›  专栏  ›  技术社区  ›  Dan

C/C++枚举:当多个项目映射到相同值时检测

  •  22
  • Dan  · 技术社区  · 14 年前

    在C/C++枚举中有检测/防止重复值的编译时方法吗?

    关键是 有多个项初始化为显式值 .

    背景:

    我继承了一些C代码,例如:

    #define BASE1_VAL    (5)
    #define BASE2_VAL    (7)
    
    typedef enum
    {
      MsgFoo1A = BASE1_VAL,       // 5
      MsgFoo1B,                   // 6
      MsgFoo1C,                   // 7
      MsgFoo1D,                   // 8
      MsgFoo1E,                   // 9
      MsgFoo2A = BASE2_VAL,       // Uh oh!  7 again...
      MsgFoo2B                    // Uh oh!  8 again...
    } FOO;
    

    问题是,随着代码的增长,随着开发人员向 MsgFoo1x 集团,最终它超支了 BASE2_VAL .

    这个代码最终会迁移到C++,所以如果只有C++的解决方案(模板魔术?)没关系,但是用C和C++工作的解决方案比较好。

    7 回复  |  直到 8 年前
        1
  •  14
  •   Dietrich Epp    8 年前

    有几种方法可以检查这个编译时,但它们可能并不总是对您有效。首先在msgfoo2a之前插入一个“marker”枚举值。

    typedef enum
    {
        MsgFoo1A = BASE1_VAL,
        MsgFoo1B,
        MsgFoo1C,
        MsgFoo1D,
        MsgFoo1E,
        MARKER_1_DONT_USE, /* Don't use this value, but leave it here.  */
        MsgFoo2A = BASE2_VAL,
        MsgFoo2B
    } FOO;
    

    现在我们需要一种方法来确保 MARKER_1_DONT_USE < BASE2_VAL 在编译时。有两种常见的技术。

    负数数组

    声明大小为负的数组是错误的。这看起来有点难看,但很管用。

    extern int IGNORE_ENUM_CHECK[MARKER_1_DONT_USE > BASE2_VAL ? -1 : 1];
    

    如果marker_1_dont_use大于base_2_val,几乎所有编写过的编译器都会生成错误。gcc指出:

    test.c:16: error: size of array ‘IGNORE_ENUM_CHECK’ is negative
    

    静态断言

    如果编译器支持c11,则可以使用 _Static_assert . 对c11的支持并不普遍,但是编译器可能支持 _静态断言 不管怎样,特别是因为C++中的相应特性被广泛支持。

    _Static_assert(MARKER_1_DONT_USE < BASE2_VAL, "Enum values overlap.");
    

    gcc发出以下消息:

    test.c:16:1: error: static assertion failed: "Enum values overlap."
     _Static_assert(MARKER_1_DONT_USE < BASE2_VAL, "Enum values overlap.");
     ^
    
        2
  •  7
  •   Community Tales Farias    7 年前

    我在您的需求中没有看到“漂亮的”,所以我提交了这个使用boost预处理器库实现的解决方案。

    作为一个预先声明,我没有使用boost.preprocessor很多次,我只在这里提供的测试用例中测试过它,所以可能会有bug,而且可能有一种更简单、更干净的方法来完成这项工作。我当然欢迎评论、更正、建议、侮辱等。

    我们走到这里:

    #include <boost/preprocessor.hpp>
    
    #define EXPAND_ENUM_VALUE(r, data, i, elem)                          \
        BOOST_PP_SEQ_ELEM(0, elem)                                       \
        BOOST_PP_IIF(                                                    \
            BOOST_PP_EQUAL(BOOST_PP_SEQ_SIZE(elem), 2),                  \
            = BOOST_PP_SEQ_ELEM(1, elem),                                \
            BOOST_PP_EMPTY())                                            \
        BOOST_PP_COMMA_IF(BOOST_PP_NOT_EQUAL(data, BOOST_PP_ADD(i, 1)))
    
    #define ADD_CASE_FOR_ENUM_VALUE(r, data, elem) \
        case BOOST_PP_SEQ_ELEM(0, elem) : break;
    
    #define DEFINE_UNIQUE_ENUM(name, values)                                  \
    enum name                                                                 \
    {                                                                         \
        BOOST_PP_SEQ_FOR_EACH_I(EXPAND_ENUM_VALUE,                            \
                                BOOST_PP_SEQ_SIZE(values), values)            \
    };                                                                        \
                                                                              \
    namespace detail                                                          \
    {                                                                         \
        void UniqueEnumSanityCheck##name()                                    \
        {                                                                     \
            switch (name())                                                   \
            {                                                                 \
                BOOST_PP_SEQ_FOR_EACH(ADD_CASE_FOR_ENUM_VALUE, name, values)  \
            }                                                                 \
        }                                                                     \
    }
    

    我们可以这样使用它:

    DEFINE_UNIQUE_ENUM(DayOfWeek, ((Monday)    (1))
                                  ((Tuesday)   (2))
                                  ((Wednesday)    )
                                  ((Thursday)  (4)))
    

    枚举器值是可选的;此代码生成的枚举等效于:

    enum DayOfWeek
    {
        Monday = 1,
        Tuesday = 2,
        Wednesday,
        Thursday = 4
    };
    

    它还生成一个健全性检查函数,该函数包含一个switch语句,如中所述 Ben Voigt's answer . 如果我们更改枚举声明使其具有非唯一的枚举器值,例如,

    DEFINE_UNIQUE_ENUM(DayOfWeek, ((Monday)    (1))
                                  ((Tuesday)   (2))
                                  ((Wednesday)    )
                                  ((Thursday)  (1)))
    

    它不会编译(VisualC++)报告预期 错误C2196:已使用大小写值“1” )

    也要感谢马蒂厄M., whose answer to another question 让我对boost预处理器库感兴趣。

        3
  •  3
  •   Billy ONeal IS4    14 年前

    我不相信有一种方法可以用语言本身检测到这一点,考虑到有些情况下,您可能希望两个枚举值相同。但是,您可以始终确保所有显式设置的项都位于列表的顶部:

    typedef enum
    {
      MsgFoo1A = BASE1_VAL,       // 5
      MsgFoo2A = BASE2_VAL,       // 7
      MsgFoo1B,                   // 8
      MsgFoo1C,                   // 9
      MsgFoo1D,                   // 10
      MsgFoo1E,                   // 11
      MsgFoo2B                    // 12
    } FOO;
    

    只要赋值在顶部,就不可能发生冲突,除非由于某种原因宏扩展为相同的值。

    通常,通过为每个msgfoox组指定固定的位数,并确保每个组不会溢出分配的位数,可以解决此问题。“位数”解决方案很好,因为它允许按位测试确定某个消息属于哪个消息组。但是没有内置的语言功能来执行此操作,因为对于具有相同值的两个枚举,存在合法的情况:

    typedef enum
    {
        gray = 4, //Gr[ae]y should be the same
        grey = 4,
        color = 5, //Also makes sense in some cases
        couleur = 5
    } FOO;
    
        4
  •  3
  •   Ben Voigt    14 年前

    我不知道有什么可以自动检查所有枚举成员,但如果您想检查初始化器(或它们所依赖的宏)的未来更改是否不会导致冲突:

    switch (0) {
        case MsgFoo1A: break;
        case MsgFoo1B: break;
        case MsgFoo1C: break;
        case MsgFoo1D: break;
        case MsgFoo1E: break;
        case MsgFoo2A: break;
        case MsgFoo2B: break;
    }
    

    如果任何一个整型值被重用,都会导致编译器错误,大多数编译器甚至会告诉您哪个值(数值)是个问题。

        5
  •  1
  •   Georg Fritzsche    14 年前

    您可以使用 Boost.Preprocessor -时间是否值得是另一回事。

    如果你正在移动到C++,也许(提议的)Boo.EnUM适合你(可以通过 Boost Vault )

    另一种方法可能是使用 gccxml (或更舒适 pygccxml )确定人工检查的候选人。

        6
  •  1
  •   Yakk - Adam Nevraumont    11 年前

    虽然我们没有完整的反射,但如果您可以重新列出枚举值,则可以解决此问题。

    在某个地方这是宣布的:

    enum E { A = 0, B = 0 };
    

    在其他地方,我们制造这种机器:

    template<typename S, S s0, S... s>
    struct first_not_same_as_rest : std::true_type {};
    template<typename S, S s0, S s1, S... s>
    struct first_not_same_as_rest : std::integral_constant< bool,
      (s0 != s1) && first_not_same_as_rest< S, s0, s... >::value
    > {};
    
    
    template<typename S, S... s>
    struct is_distinct : std::true_type {};
    
    template<typename S, S s0, S... s>
    struct is_distinct : std::integral_constant< bool,
      std::is_distinct<S, s...>::value &&
      first_not_same_as_rest< S, s0, s... >::value
    > {};
    

    一旦你有了这台机器(它需要C++ 11),我们可以做如下操作:

    static_assert( is_distinct< E, A, B >::value, "duplicate values in E detected" );
    

    在编译时,我们将确保没有两个元素是相等的。

    这需要o(n)递归深度和o(n^2)编译器在编译时工作,因此对于非常大的枚举,这可能会导致问题。o(lg(n))深度和o(n lg(n))常数因子大得多的功可以通过首先对元素列表进行排序来完成,但这是非常多的工作。

    对于C++ 17提出的枚举反射代码,在不影响元素的情况下,这是可行的。

        7
  •  0
  •   Community Tales Farias    7 年前

    我不完全喜欢这里已经发布的任何答案,但他们给了我一些想法。关键技术是依赖ben voight使用switch语句的答案。如果一个交换机中有多个实例共享同一个数字,则会出现编译错误。

    最有用的是我自己和可能的原始海报,这不需要任何C++特性。

    为了把事情弄清楚,我用了Aaronps的答案 How can I avoid repeating myself when creating a C++ enum and a dependent data structure?

    首先,在某个地方定义这个标题:

    #define DEFINE_ENUM_VALUE(name, value)      name=value,
    #define CHECK_ENUM_VALUE(name, value)       case name:
    #define DEFINE_ENUM(enum_name, enum_values) \
        typedef enum { enum_values(DEFINE_ENUM_VALUE) } enum_name;
    #define CHECK_ENUM(enum_name, enum_values) \
        void enum_name ## _test (void) { switch(0) { enum_values(CHECK_ENUM_VALUE); } }
    

    现在,每当需要枚举时:

    #define COLOR_VALUES(GEN) \
        GEN(Red, 1) \
        GEN(Green, 2) \
        GEN(Blue, 2)
    

    最后,需要这些行才能实际进行枚举:

    DEFINE_ENUM(Color, COLOR_VALUES)
    CHECK_ENUM(Color, COLOR_VALUES)
    

    DEFINE_ENUM 使枚举数据类型本身。 CHECK_ENUM 生成一个测试函数,用于打开所有枚举值。编译时编译器将崩溃 支票所有权 如果你有副本的话。