如何创建类型安全的枚举?

How to create type safe enums?

在 C 中使用枚举实现类型安全是有问题的,因为它们本质上只是整数。而枚举常量实际上被标准定义为 int 类型。

为了实现一点类型安全,我用这样的指针做了一些技巧:

typedef enum
{
  BLUE,
  RED
} color_t;

void color_assign (color_t* var, color_t val) 
{ 
  *var = val; 
}

因为指针比值有更严格的类型规则,所以这可以防止这样的代码:

int x; 
color_assign(&x, BLUE); // compiler error

但它不会阻止这样的代码:

color_t color;
color_assign(&color, 123); // garbage value

这是因为枚举常量本质上只是一个 int 并且可以隐式分配给枚举变量。

有没有办法编写这样的函数或宏color_assign,即使对于枚举常量也能实现完全类型安全?

可以通过一些技巧来实现。鉴于

typedef enum
{
  BLUE,
  RED
} color_t;

然后定义一个虚拟联合,它不会被调用者使用,但包含与枚举常量同名的成员:

typedef union
{
  color_t BLUE;
  color_t RED;
} typesafe_color_t;

这是可能的,因为枚举常量和 member/variable 名称驻留在不同的命名空间中。

然后制作一些类似函数的宏:

#define c_assign(var, val) (var) = (typesafe_color_t){ .val = val }.val
#define color_assign(var, val) _Generic((var), color_t: c_assign(var, val))

然后像这样调用这些宏:

color_t color;
color_assign(color, BLUE); 

解释:

  • C11 _Generic关键字确保枚举变量的类型正确。但是,这不能用在枚举常量 BLUE 上,因为它的类型是 int.
  • 因此,辅助宏 c_assign 创建了虚拟联合的临时实例,其中指定的初始化语法用于将值 BLUE 分配给名为 BLUE 的联合成员。如果不存在这样的成员,代码将不会编译。
  • 然后将相应类型的联合成员复制到枚举变量中。

我们实际上不需要辅助宏,我只是为了便于阅读而拆分了表达式。写

一样好用
#define color_assign(var, val) _Generic((var), \
color_t: (var) = (typesafe_color_t){ .val = val }.val )

示例:

color_t color; 
color_assign(color, BLUE);// ok
color_assign(color, RED); // ok

color_assign(color, 0);   // compiler error 

int x;
color_assign(x, BLUE);    // compiler error

typedef enum { foo } bar;
color_assign(color, foo); // compiler error
color_assign(bar, BLUE);  // compiler error

编辑

显然,上面的内容并不能阻止调用者简单地键入 color = garbage;。如果您希望完全阻止使用这种枚举分配的可能性,您可以将它放在一个结构中并使用标准的私有封装过程 "opaque type":

color.h

#include <stdlib.h>

typedef enum
{
  BLUE,
  RED
} color_t;

typedef union
{
  color_t BLUE;
  color_t RED;
} typesafe_color_t;

typedef struct col_t col_t; // opaque type

col_t* col_alloc (void);
void   col_free (col_t* col);

void col_assign (col_t* col, color_t color);

#define color_assign(var, val)   \
  _Generic( (var),               \
    col_t*: col_assign((var), (typesafe_color_t){ .val = val }.val) \
  )

color.c

#include "color.h"

struct col_t
{
  color_t color;
};

col_t* col_alloc (void) 
{ 
  return malloc(sizeof(col_t)); // (needs proper error handling)
}

void col_free (col_t* col)
{
  free(col);
}

void col_assign (col_t* col, color_t color)
{
  col->color = color;
}

main.c

col_t* color;
color = col_alloc();

color_assign(color, BLUE); 

col_free(color);

可以使用 struct:

强制类型安全
struct color { enum { THE_COLOR_BLUE, THE_COLOR_RED } value; };
const struct color BLUE = { THE_COLOR_BLUE };
const struct color RED  = { THE_COLOR_RED  };

由于 color 只是一个包装整数,它可以通过值或指针传递,就像使用 int 一样。使用 color 的定义,color_assign(&val, 3); 无法编译:

error: incompatible type for argument 2 of 'color_assign'

     color_assign(&val, 3);
                        ^

完整(工作)示例:

struct color { enum { THE_COLOR_BLUE, THE_COLOR_RED } value; };
const struct color BLUE = { THE_COLOR_BLUE };
const struct color RED  = { THE_COLOR_RED  };

void color_assign (struct color* var, struct color val) 
{ 
  var->value = val.value; 
}

const char* color_name(struct color val)
{
  switch (val.value)
  {
    case THE_COLOR_BLUE: return "BLUE";
    case THE_COLOR_RED:  return "RED";
    default:             return "?";
  }
}

int main(void)
{
  struct color val;
  color_assign(&val, BLUE);
  printf("color name: %s\n", color_name(val)); // prints "BLUE"
}

Play with in online (demo).

最终,当您使用无效的枚举值时,您想要的是警告或错误。

正如你所说,C语言做不到这一点。然而,您可以轻松地使用静态分析工具来解决这个问题——Clang 显然是免费的,但还有很多其他工具。不管语言是否类型安全,静态分析都可以检测并报告问题。通常静态分析工具会发出警告,而不是错误,但您可以轻松地让静态分析工具报告错误而不是警告,并更改您的 makefile 或构建项目来处理此问题。

最佳答案非常好,但它的缺点是它需要大量的 C99 和 C11 功能集才能编译,而且最重要的是,它使分配非常不自然:你必须使用magic color_assign() 函数或宏来代替标准 = 运算符来移动数据。

(不可否认,这个问题明确询问了 如何 来写 color_assign(),但是如果你更广泛地看这个问题,它实际上是关于如何更改你的代码通过某种形式的枚举常量获得类型安全,我认为首先不需要 color_assign() 来获得类型安全是公平的答案。)

指针是 C 视为类型安全的少数形状之一,因此它们自然成为解决此问题的候选对象。所以我会这样攻击它:与其使用 enum,我会牺牲一点内存来获得唯一的、可预测的指针值,然后使用一些非常古怪的 #define 语句构造我的 "enum"(是的,我知道宏会污染宏命名空间,但是 enum 会污染编译器的全局命名空间,所以我认为它接近于平衡交易):

color.h:

typedef struct color_struct_t *color_t;

struct color_struct_t { char dummy; };

extern struct color_struct_t color_dummy_array[];

#define UNIQUE_COLOR(value) \
    (&color_dummy_array[value])

#define RED    UNIQUE_COLOR(0)
#define GREEN  UNIQUE_COLOR(1)
#define BLUE   UNIQUE_COLOR(2)

enum { MAX_COLOR_VALUE = 2 };

当然,这确实需要您在某处保留足够的内存以确保没有其他任何东西可以使用这些指针值:

color.c:

#include "color.h"

/* This never actually gets used, but we need to declare enough space in the
 * BSS so that the pointer values can be unique and not accidentally reused
 * by anything else. */
struct color_struct_t color_dummy_array[MAX_COLOR_VALUE + 1];

但从消费者的角度来看,这一切都是隐藏的:color_t几乎是一个不透明的物体。除了有效的 color_t 值和 NULL:

之外,你不能给它分配任何东西

user.c:

#include <stddef.h>
#include "color.h"

void foo(void)
{
    color_t color = RED;    /* OK */
    color_t color = GREEN;  /* OK */
    color_t color = NULL;   /* OK */
    color_t color = 27;     /* Error/warning */
}

这在大多数情况下效果很好,但它确实存在在switch语句中不起作用的问题;你不能 switch 在指针上(这是一种耻辱)。但是,如果您愿意再添加一个宏以使切换成为可能,那么您可以获得 "good enough":

color.h:

...

#define COLOR_NUMBER(c) \
    ((c) - color_dummy_array)

user.c:

...

void bar(color_t c)
{
    switch (COLOR_NUMBER(c)) {
        case COLOR_NUMBER(RED):
            break;
        case COLOR_NUMBER(GREEN):
            break;
        case COLOR_NUMBER(BLUE):
            break;
    }
}

这是好的解决方案吗?我不会称它为 great,因为它既浪费了一些内存又污染了宏命名空间,而且它不允许你使用 enum 来自动分配你的颜色值,但它 另一种解决问题的方法,它的用法更自然,而且与最佳答案不同,它一直适用于 C89。