如何创buildtypes安全的枚举?
在C中使用枚举来实现types安全是有问题的,因为它们实质上只是整数。 枚举常量实际上被定义为由标准的int
types。
为了达到某种types的安全性,我使用这样的指针做技巧:
typedef enum { BLUE, RED } color_t; void color_assign (color_t* var, color_t val) { *var = val; }
因为指针比值更严格的types规则,所以这可以防止这样的代码:
int x; color_assign(&x, BLUE); // compiler error
但是这并不妨碍这样的代码:
color_t color; color_assign(&color, 123); // garbage value
这是因为枚举常量本质上只是一个int
并可以隐式地分配给一个枚举variables。
有没有办法写这样一个函数或macroscolor_assign
,甚至可以实现完整的types安全枚举常量?
有一些技巧可以做到这一点。 特定
typedef enum { BLUE, RED } color_t;
然后定义一个不会被调用者使用的虚拟联合,但包含与枚举常量同名的成员:
typedef union { color_t BLUE; color_t RED; } typesafe_color_t;
这是可能的,因为枚举常量和成员/variables名称驻留在不同的名称空间中。
然后制作一些类似function的macros:
#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))
这些macros然后像这样调用:
color_t color; color_assign(color, BLUE);
说明:
- C11
_Generic
关键字确保枚举variables的types正确。 但是,这不能用于枚举常量BLUE
因为它是int
types的。 - 因此,助手macros
c_assign
创build虚拟联合的临时实例,其中使用指定的初始化程序语法将值BLUE
分配给名为BLUE
的联合成员。 如果不存在这样的成员,代码将不会被编译。 - 然后将相应types的联合成员复制到枚举variables中。
我们其实不需要助手macros,我只是为了可读性而拆分expression式。 它的工作原理还不错
#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
编辑
显然,上面的内容并不妨碍调用者简单地inputcolor = garbage;
。 如果你想完全阻止使用这种枚举赋值的可能性,你可以把它放在一个结构体中,并使用“不透明types”的私有封装的标准过程:
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);
最好的答案是相当不错的,但它的缺点是需要大量的C99和C11function才能编译,最重要的是,它使得赋值非常不自然:你必须使用color_assign()
函数或macros来移动数据而不是标准=
运算符。
(不可否认,这个问题明确地问到如何编写color_assign()
,但是如果你更广泛地看待这个问题,那么关于如何改变你的代码来使用某种forms的枚举常量来获得types安全性,我会考虑首先不需要color_assign()
来获得types安全性是公平游戏的答案。)
指针是C对待types安全的几个形状之一,所以它们是解决这个问题的自然人选。 所以我会这样攻击它:而不是使用一个enum
,我会牺牲一点记忆,以便能够具有独特的,可预测的指针值,然后使用一些真正有趣的#define
语句来构造我的“枚举”(是的,我知道macros会污染macros命名空间,但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];
但从消费者的angular度来看,这一切都是隐藏的: 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
一个指针(这是一个耻辱)。 但是,如果你愿意增加一个macros来切换,你可以得到一个“足够好”的东西:
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; } }
这是一个很好的解决scheme? 我不会称它为好 ,因为它既浪费了一些内存,又污染了macros命名空间,并且不允许你使用enum
来自动分配你的颜色值,但它是另一种解决问题的方法,自然的用法,而不像顶级的答案,它一直工作回到C89。
最终,当你使用一个无效的枚举值时,你想要的是一个警告或错误。
正如你所说,C语言不能做到这一点。 然而,你可以很容易地使用静态分析工具来解决这个问题 – 铿锵是明显的自由的,但也有很多其他的。 无论语言是否是types安全的,静态分析都可以检测并报告问题。 通常情况下,静态分析工具会提示警告,而不会出现错误,但您可以轻松地让静态分析工具报告错误而不是警告,并更改您的生成文件或生成项目来处理此问题。
我们可以用一个struct
来强制types安全:
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);
无法编译:
错误:“color_assign”参数2的不兼容types
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" }
在网上玩(演示) 。
这是我的解决scheme。 我命名空间的一切,所以我的枚举将被命名为:
ns_a_e //ns = namespace; a=enum name; e = it's an enum
其成员的范围如下:
enum ns_a_e { ns_a_e__a, ns_a_e__b, ns_a_e__c, };
所以我想了一个可能修改上面的:
typedef struct { enum { ns_a_e__a, ns_a_e__b, ns_a_e__c, } x; /*note: C doesn't scope the enumerators*/ } ns_a_e;
然后我可以用类似的东西创build实例:
#define NS_e(En,Val) (En){En##__##Val}
或(伪)枚举特定的
#define NS_a_e(Val) NS_e(ns_a_e,Val)
这个简单的修改,我写NS_a_e(a)
而不是我以前的ns_a_e__a
使这些(伪)枚举完全types安全:
ns_a_e x; x = NS_a_e(a); x = NS_a_e(b); //x = NS_a_e(foo); //ERROR //x = 1; //ERROR
只要我的代码的其他地方尊重范围(= ns_a_e__*
forms的符号属于ns_a_e
)
结构包装不会影响在x86-64上生成的程序集。
完整的例子(与-std=c90
编译):
#define NS_e(En,X) (En){En##__##X} ///////////////////////// typedef struct{ enum { ns_a_e__a, ns_a_e__b, ns_a_e__c, } x; } ns_a_e; #define NS_a_e(X) NS_e(ns_a_e, X) int main() { ns_a_e x; x = NS_a_e(a); x = NS_a_e(b); //x = NS_a_e(foo); //ERROR //x = 1; //ERROR }