如何创建类型安全枚举?

时间:2017-03-27 09:54:11

标签: c enums type-safety

使用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,即使对于枚举常量也可以实现完整的类型安全性?

4 个答案:

答案 0 :(得分:51)

通过一些技巧可以实现这一目标。给定

typedef enum
{
  BLUE,
  RED
} color_t;

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

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

这是可能的,因为枚举常量和成员/变量名称位于不同的名称空间中。

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

#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;。如果你希望完全阻止使用枚举的这种赋值的可能性,你可以把它放在一个结构中并使用私有封装的标准过程与&#34; opaque类型&#34; :< / p>

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);

答案 1 :(得分:8)

最佳答案非常好,但它有缺点,它需要很多C99和C11功能集才能编译,而且最重要的是,它使得赋值非常不自然:你必须使用魔法{ {1}}函数或宏,以便移动数据而不是标准color_assign()运算符。

(不可否认,这个问题明确询问 如何编写=,但如果你更广泛地看待这个问题,那就是如何更改代码以获得类型安全性使用某种形式的枚举常量,我认为首先不需要color_assign()才能使类型安全成为公平游戏的答案。)

指针是C对待类型安全的少数形状之一,因此它们成为解决此问题的自然候选者。所以我会用这种方式攻击它:不是使用color_assign(),而是牺牲一点内存以便能够拥有唯一的,可预测的指针值,然后使用一些非常好的时髦enum语句构造我的“枚举”(是的,我知道宏污染了宏命名空间,但是#define污染了编译器的全局命名空间,所以我认为它接近于偶数交易):

<强> color.h

enum

当然,这确实要求您在某处保留足够的内存以确保其他任何内容都不会占用这些指针值:

<强> color.c

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 };

但从消费者的角度来看,这一切都是隐藏的:#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值和NULL之外,您不能为其分配任何内容:

<强> user.c的

color_t

这在大多数情况下效果很好,但它确实存在不在#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指针(这是一个耻辱)。但是如果你愿意再添加一个宏来实现切换,你可以得到一些“足够好”的东西:

<强> color.h

switch

<强> user.c的

...

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

这是一个好的解决方案吗?我不会称之为很好,因为它既浪费了一些内存又污染了宏命名空间,并且它不允许你使用... void bar(color_t c) { switch (COLOR_NUMBER(c)) { case COLOR_NUMBER(RED): break; case COLOR_NUMBER(GREEN): break; case COLOR_NUMBER(BLUE): break; } } 自动分配你的颜色值,但它< em>是另一种解决问题的方法,它可以产生更自然的用法,与顶级答案不同,它可以一直回到C89。

答案 2 :(得分:7)

可以使用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);无法编译:

  

错误:&#39; color_assign&#39;

的参数2的不兼容类型
     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)

答案 3 :(得分:7)

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

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