【问题标题】:How to create type safe enums?如何创建类型安全的枚举?
【发布时间】:2017-08-19 23:22:42
【问题描述】:

在 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,即使对于枚举常量也能实现完全的类型安全?

【问题讨论】:

标签: c enums type-safety


【解决方案1】:

可以通过一些技巧来实现这一点。给定

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;。如果您希望完全阻止使用这种枚举分配的可能性,您可以将它放在一个结构中并使用带有 "opaque type" 的私有封装的标准过程:

颜色.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) \
  )

颜色.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);

【讨论】:

  • 这真的很可爱,虽然它不会发现一些错误:int zonk(int x) {color_t color; color = x; return color;}
  • @gsg 您显然必须禁止直接分配。这可以通过例如将枚举嵌入到结构中,然后使结构成为不透明类型来实现。
  • @gsg 我添加了一个带有私有封装的示例,它阻止了直接分配。
  • 我是否遗漏了什么,或者无法将color_assign 与来自变量、类型或其他变量的值一起使用?由于宏也使用“表达式”作为字段名称。您实际上如何以类型安全的方式处理这些值?
  • @anicicn 它通过所谓的复合文字(C99特性)创建联合类型的临时变量,然后初始化这个临时联合变量的特定成员(通过指定的初始化器,另一个 C99 特性)。如果联合中不存在具有匹配名称的成员,则代码将无法编译。如果成员匹配,例如 RED,联合成员 RED 将被分配值 RED。通过在最后输入.val,代码访问该成员并将其复制到目标变量中。在实践中,我相信大部分代码都会被优化掉。
【解决方案2】:

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

(诚然,该问题明确询问了如何编写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(这很遗憾)。但是,如果您愿意再添加一个宏以使切换成为可能,那么您可以得到“足够好”的东西:

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。

【讨论】:

  • 使用 C11 功能并不是一个合理的缺点。
  • 如果您的编译器不支持 C11 功能,这是一个缺点。我不会说出任何名字(coughMicrosoftcough),但有许多“C”编译器无法处理 C11。
  • 有趣的想法。您应该考虑完全隐藏结构定义,因此没有人会想到使用它或访问成员。这可以使用不透明类型来完成,如我的回答中的编辑所示。此外,如果您声明结构const,您不会在 .bss 中浪费空间,而是在 .rodata 或类似的东西中浪费空间。那么color_t color = 0; 呢?或者更糟:任何在编译时计算为 0 的表达式。
  • const 绝对是个好主意;将它放在文本/只读段中值得付出更多努力。也就是说,在类似情况下,我经常将内部属性命名为 opaque_,这当然不是万无一失的,但在过去已经足够好,可以让脏手远离饼干罐。
  • 至于零问题,这是一个问题,可以肯定的是,但这是一个与 C 中所有其他指针类型共享的问题:是的,你可以写 color = 0,但它不是与color = BLACK 相同,string = 0string = "" 相同。 switch 语句甚至可以识别NULL 并在其default 情况下处理它,或者甚至有NULL 本身的特殊情况。你最终会得到一个有效地具有额外非值的枚举,但考虑到有多少真实世界的枚举已经具有类似 DEFAULT = 0 的东西,我认为这不会损害该技术。
【解决方案3】:

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

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

【讨论】:

  • 显然静态分析器始终是一个选项,例如 MISRA-C:2012 检查器将捕获枚举类型问题。市场上所有静态分析器的主要问题是它们充满了错误/“误报”,以至于它们不是很有用。如果您可以通过任何标准 C 编译器强制进行编译器诊断,那始终是首选解决方案。
  • @Lundin 我对静态分析的经验并不是它充满了错误,而是惯用的 C 会经常破坏编码标准——例如,“if(ptr)”作为非 NULL 的检查.静态分析的大部分工作都必须用于完善您的规则集。 OTOH,一旦你这样做了,那么你就有了一个非常强大的工具,可以真正改进你的代码。
  • @Lundin 在代码中添加冗余函数和宏似乎会增加复杂性,最终降低代码质量。恕我直言,花在实现和修改以前的代码上的时间最好使用静态分析工具。
  • @Graham if(ptr) 比惯用的 if(ptr != NULL) 是相当草率但广泛的实践。无论如何,这不是大多数静态分析器不好的原因,而是像type x; type_init(&amp;x); 这样的场景,然后你会得到“警告!x 在传递给函数时没有初始化!”。是的...感谢您让我知道我的变量在初始化之前没有被初始化。例如,未能正确分析跨翻译单元。
  • @B.Wolf 理想情况下,您将有多种预防错误的方法。如果你有编译时断言手动代码审查静态分析,你会比没有所有这些时提高代码质量。
【解决方案4】:

可以使用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(&amp;val, 3); 无法编译:

错误:“color_assign”的参数 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).

【讨论】:

  • 我相信这会迫使您为枚举和 const 结构使用不同的名称。
  • @Lundin 确实如此:每种颜色都有一个private(或internal)名称(enum)和一个public i> 一个 (const struct)。我不认为这是一个缺点。
猜你喜欢
  • 2016-06-08
  • 1970-01-01
  • 1970-01-01
  • 2010-09-07
  • 2015-02-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-03
相关资源
最近更新 更多