如何创建类型安全枚举?

Lun*_*din 54 c enums type-safety

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

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

typedef enum
{
  BLUE,
  RED
} color_t;

void color_assign (color_t* var, color_t val) 
{ 
  *var = val; 
}
Run Code Online (Sandbox Code Playgroud)

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

int x; 
color_assign(&x, BLUE); // compiler error
Run Code Online (Sandbox Code Playgroud)

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

color_t color;
color_assign(&color, 123); // garbage value
Run Code Online (Sandbox Code Playgroud)

这是因为枚举常量基本上只是一个int,可以隐式赋值给枚举变量.

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

Lun*_*din 52

只需一些技巧就可以实现这一目标.特定

typedef enum
{
  BLUE,
  RED
} color_t;
Run Code Online (Sandbox Code Playgroud)

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

typedef union
{
  color_t BLUE;
  color_t RED;
} typesafe_color_t;
Run Code Online (Sandbox Code Playgroud)

这是可能的,因为枚举常量和成员/变量名称驻留在不同的名称空间中.

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

#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))
Run Code Online (Sandbox Code Playgroud)

然后像这样调用这些宏:

color_t color;
color_assign(color, BLUE); 
Run Code Online (Sandbox Code Playgroud)

说明:

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

我们实际上不需要辅助宏,我只是将表达式拆分为可读性.它的工作原理同样好

#define color_assign(var, val) _Generic((var), \
color_t: (var) = (typesafe_color_t){ .val = val }.val )
Run Code Online (Sandbox Code Playgroud)

例子:

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
Run Code Online (Sandbox Code Playgroud)

编辑

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

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) \
  )
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

main.c中

col_t* color;
color = col_alloc();

color_assign(color, BLUE); 

col_free(color);
Run Code Online (Sandbox Code Playgroud)

  • @LPs Nah,我不喜欢Documentation项目.那里的废话太多了. (35认同)
  • 您也应该将其添加到文档中. (3认同)

Sea*_*ema 8

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

(诚然,问题明确地询问如何color_assign(),但如果你的问题看起来更广泛地说,它的的确确是如何改变你的代码来获得类型安全某种形式枚举常数的,而且我认为不需要color_assign()在第一个让类型安全成为公平游戏的答案.)

指针是C对待类型安全的少数形状之一,因此它们成为解决此问题的自然候选者.所以我会用这种方式攻击它:而不是使用一个enum,我会牺牲一点内存,以便能够拥有独特的,可预测的指针值,然后使用一些非常好的时髦#define语句构建我的"枚举"(是的,我知道宏污染了宏命名空间,但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 };
Run Code Online (Sandbox Code Playgroud)

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

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];
Run Code Online (Sandbox Code Playgroud)

但从消费者的角度来看,这一切都是隐藏的: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 */
}
Run Code Online (Sandbox Code Playgroud)

这在大多数情况下效果很好,但确实存在不能在switch语句中工作的问题; 你不能switch指针(这是一个耻辱).但是如果你愿意再添加一个宏来实现切换,你可以得到一些"足够好"的东西:

color.h:

...

#define COLOR_NUMBER(c) \
    ((c) - color_dummy_array)
Run Code Online (Sandbox Code Playgroud)

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个好的解决方案吗?我不会称它为,因为它既浪费了一些内存并污染了宏命名空间,也不会让你enum用来自动分配你的颜色值,但它另一种解决问题的方法,它会导致更自然用法,与最佳答案不同,它一直可以回到C89.

  • 如果您的编译器不支持C11功能,那么这是一个缺点.我不会说出任何名字(*咳嗽*微软*咳嗽*),但有一些"C"编译器无法处理C11. (7认同)
  • 使用 C11 功能并不是一个合理的缺点。 (2认同)

YSC*_*YSC 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  };
Run Code Online (Sandbox Code Playgroud)

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

错误:'color_assign'的参数2的不兼容类型

     color_assign(&val, 3);
                        ^
Run Code Online (Sandbox Code Playgroud)

完整(工作)示例:

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"
}
Run Code Online (Sandbox Code Playgroud)

在线玩(演示).


Gra*_*ham 7

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

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

  • @Lundin我对静态分析的体验并不是它充满了bug,而是惯用的C会经常破坏编码标准 - 例如,"if(ptr)"作为非NULL的检查.静态分析的大部分工作都必须用于改进规则集.OTOH,一旦你完成了这个,那么你就拥有了一个非常强大的工具,可以真正改善你的代码. (2认同)