【问题标题】:Typesafe enums in C?C中的类型安全枚举?
【发布时间】:2013-02-11 22:45:30
【问题描述】:

如果我有多个enum,例如:

 enum Greetings{ hello, bye, how };

 enum Testing { one, two, three };

如何强制使用正确的enum? 例如,我不希望有人在应该使用one 以获得更好的调试和可读性时使用hello

【问题讨论】:

    标签: c enums


    【解决方案1】:

    在 C 中,您可以使用样板代码来伪造它。

    typedef enum { HELLO_E, GOODBYE_E } greetings_t;
    struct greetings { greetings_t greetings; };
    #define HELLO ((struct greetings){HELLO_E})
    #define GOODBYE ((struct greetings){GOODBYE_E})
    
    typedef enum { ONE_E, TWO_E } number_t;
    struct number { number_t number; };
    #define ONE ((struct number){ONE_E})
    #define TWO ((struct number){TWO_E})
    
    void takes_greeting(struct greetings g);
    void takes_number(struct number n);
    
    void test()
    {
        takes_greeting(HELLO);
        takes_number(ONE);
    
        takes_greeting(TWO);
        takes_number(GOODBYE);
    }
    

    这不会产生任何开销,并且会产生错误而不是警告:

    $ gcc -c -std=c99 -Wall -Wextra test2.c test2.c:在函数“测试”中: test2.c:19:错误:“takes_greeting”的参数 1 的类型不兼容 test2.c:20:错误:“takes_number”的参数 1 的类型不兼容

    请注意,我没有使用 GNU 扩展,并且不会生成虚假警告。只有错误。另请注意,我使用的 GCC 版本与泥土一样古老,

    $ gcc --版本 powerpc-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc. build 5493) 版权所有 (C) 2005 Free Software Foundation, Inc. 这是免费软件;查看复制条件的来源。没有 保修单;甚至不考虑适销性或特定用途的适用性。

    这应该适用于任何支持 C99 复合文字的编译器。

    【讨论】:

    • 这很简洁,但确实使在 switch 语句中使用枚举有点难看。
    【解决方案2】:

    Clang 会产生以下警告,这是您能做的最好的事情(尽管用户可以将警告升级为错误)。

    enum Greetings { hello, bye, how };
    enum Count { one, two, three };
    
    void takes_greeting(enum Greetings x) {}
    void takes_count(enum Count x) {}
    
    int main() {
        takes_greeting(one);
        takes_count(hello);
    }
    

    编译器输出:

    cc     foo.c   -o foo
    foo.c:8:17: warning: implicit conversion from enumeration type 'enum Count' to different enumeration type 'enum Greetings' [-Wenum-conversion]
            takes_greeting(one);
            ~~~~~~~~~~~~~~ ^~~
    foo.c:9:14: warning: implicit conversion from enumeration type 'enum Greetings' to different enumeration type 'enum Count' [-Wenum-conversion]
            takes_count(hello);
            ~~~~~~~~~~~ ^~~~~
    

    如果用户要忽略来自编译器的错误和警告,那么您就无能为力了。

    【讨论】:

    • 遗憾的是,gcc 在 C 模式下不会产生类似的警告。我不知道 msvc 有没有。有一些 cmets here 关于如何欺骗 gcc 产生警告,以牺牲可读性为代价。
    【解决方案3】:

    不幸的是,enum 是 C 类型系统中的一个弱点。enum 类型的变量属于enum 类型,但您使用enum 声明的常量属于int 类型.

    所以在你的例子中

    enum Greetings{ hello, bye, how };
    enum Testing { one, two, three };
    
    enum Greetings const holla = hello;
    enum Testing const eins = one;
    

    helloone是同一个值的两个名字,即0,同类型int

    hollaeins 再次具有值 0,但有各自的类型。

    如果您想为“真实”常量强制一些“官方”类型安全,即具有您想要的类型和值的实体,您必须使用一些更复杂的构造:

    #define GREETING(VAL) ((enum Greetings){ 0 } = (VAL))
    #define HELLO GREETING(hello)
    

    GREETING 宏中的赋值确保结果是一个“右值”,因此它不能被修改,编译器只会根据它的类型和值来获取它。

    【讨论】:

      【解决方案4】:

      这是你不想听到的答案。在 C 中,你真的不能。现在,如果您的 C 代码位于 C++ 的“清洁 C”子集中,您可以使用 C++ 编译器进行编译,以获取使用错误枚举/int 值等的所有错误。

      【讨论】:

      • 即使在 C++ 中也不是很简单:你需要 C++11 的“枚举类”特性,然后在任何地方指定类名——例如foo = Greetings::hello;
      【解决方案5】:

      如果您还想确保有效范围,可以使用一种技术,该技术具有用于获取整数值的指针取消引用的少量开销——以及大量样板类型。它可能仍然有用,因为它可以帮助您编写范围检查代码,否则它是必要的。

      greetings.h:

      #ifndef GREETINGS_H
      #define GREETINGS_H
      
      struct greetings;
      typedef struct greetings Greetings;
      
      extern const Greetings * const Greetings_hello;
      extern const Greetings * const Greetings_bye;
      extern const Greetings * const Greetings_how;
      
      const char *Greetings_str(const Greetings *g);
      int Greetings_int(const Greetings *g);
      
      #endif
      

      greetings.c:

      #include "greetings.h"
      
      struct greetings {
          const int val;
      };
      
      static const Greetings hello = { 0 };
      static const Greetings bye = { 1 };
      static const Greetings how = { 2 };
      
      const Greetings * const Greetings_hello = &hello;
      const Greetings * const Greetings_bye = &bye;
      const Greetings * const Greetings_how = &how;
      
      static const char * const Greetings_names[] = {
          "hello",
          "bye",
          "how"
      };
      
      const char *
      Greetings_str(const Greetings *g)
      {
          return Greetings_names[g->val];
      }
      
      int
      Greetings_int(const Greetings *g)
      {
          return g->val;
      }
      

      示例 main.c:

      #include <stdio.h>
      #include "greetings.h"
      
      void
      printTest(const Greetings *greeting)
      {
          if (greeting == Greetings_how) return;
          puts(Greetings_str(greeting));
      }
      
      int
      main()
      {
          const Greetings *g = Greetings_hello;
          printTest(g);
      }
      

      是的,要输入很多内容,但您可以获得完整的类型和范围安全性。没有其他编译单元可以实例化struct greetings,因此您是完全安全的。


      编辑 2015-07-04:为了防止 NULL,有两种可能性。要么使用 NULL 作为默认值(#define Greetings_hello 0 而不是现在使用的指针)。这非常方便,但会降低默认枚举值的类型安全性,NULL 可用于任何枚举。或者声明它无效,然后在访问器方法中检查它,返回错误,或者使用 GCCs __attribute__((nonnull())) 之类的东西在编译时捕获它,例如在 greetings.h 中:

      const char *Greetings_str(const Greetings *g)
              __attribute__((nonnull(1)));
      

      【讨论】:

      • 如果用户传入null,最好尽快崩溃,让他们解决。或者,正如您所说,使用特定于编译器的技巧。我可以建议的一个小改进是将问候字符串移动到Greetings 结构中,因为无论如何您都将它们视为实例成员。所以如果你有例如static const Greetings hello = { 0, "hello" };,等等,你可以在实现中只使用return g-&gt;label
      【解决方案6】:

      您可以对枚举进行 typedef,然后声明这些类型的变量和函数参数。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-12-30
        • 1970-01-01
        相关资源
        最近更新 更多