example enum c enums type-safety

c - example - enum java



¿Cómo crear enumeraciones seguras de tipo? (4)

Lograr la seguridad de tipos con enumeraciones en C es problemático, ya que son esencialmente enteros. Y las constantes de enumeración se definen de hecho como de tipo int por el estándar.

Para lograr un poco de seguridad de tipo, hago trucos con punteros como este:

typedef enum { BLUE, RED } color_t; void color_assign (color_t* var, color_t val) { *var = val; }

Debido a que los punteros tienen reglas de tipo más estrictas que los valores, esto evita códigos como este:

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

Pero no impide un código como este:

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

Esto se debe a que la constante de enumeración es esencialmente solo un int y puede asignarse implícitamente a una variable de enumeración.

¿Hay alguna manera de escribir tal función o macro color_assign , que pueda lograr una seguridad de tipo completa incluso para las constantes de enumeración?


En última instancia, lo que desea es una advertencia o un error cuando utiliza un valor de enumeración no válido.

Como dices, el lenguaje C no puede hacer esto. Sin embargo, puede usar fácilmente una herramienta de análisis estático para detectar este problema: Clang es el obvio gratuito, pero hay muchos otros. Independientemente de si el lenguaje es de tipo seguro, el análisis estático puede detectar e informar el problema. Por lo general, una herramienta de análisis estático muestra advertencias, no errores, pero puede hacer que la herramienta de análisis estático informe un error en lugar de una advertencia y cambie su archivo MAKE o proyecto de compilación para manejar esto.


Es posible lograr esto con algunos trucos. Dado

typedef enum { BLUE, RED } color_t;

Luego defina una unión ficticia que no será utilizada por la persona que llama, pero que contiene miembros con los mismos nombres que las constantes de enumeración:

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

Esto es posible porque las constantes de enumeración y los nombres de miembros / variables residen en espacios de nombres diferentes.

Luego haga algunas macros con función:

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

Estas macros se llaman así:

color_t color; color_assign(color, BLUE);

Explicación:

  • La palabra clave C11 _Generic garantiza que la variable de enumeración sea del tipo correcto. Sin embargo, esto no se puede usar en la constante de enumeración BLUE porque es de tipo int .
  • Por lo tanto, la macro auxiliar c_assign crea una instancia temporal de la unión ficticia, donde la sintaxis del inicializador designado se usa para asignar el valor BLUE a un miembro de la unión llamado BLUE . Si no existe tal miembro, el código no se compilará.
  • El miembro de unión del tipo correspondiente se copia en la variable enum.

En realidad no necesitamos la macro auxiliar, simplemente dividí la expresión para facilitar la lectura. Funciona igual de bien escribir

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

Ejemplos:

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

EDITAR

Obviamente, lo anterior no impide que la persona que llama simplemente escriba color = garbage; . Si desea bloquear por completo la posibilidad de utilizar dicha asignación de la enumeración, puede ponerla en una estructura y utilizar el procedimiento estándar de encapsulación privada con "tipo opaco" :

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

C Principal

col_t* color; color = col_alloc(); color_assign(color, BLUE); col_free(color);


La respuesta principal es bastante buena, pero tiene las desventajas de que requiere una gran cantidad de funciones C99 y C11 configuradas para compilar, y además, hace que la asignación sea poco natural: debe usar una función mágica color_assign() o macro para mover datos en lugar del operador estándar = .

(Es cierto que la pregunta se hizo explícitamente sobre cómo escribir color_assign() , pero si observa la pregunta de manera más amplia, realmente se trata de cómo cambiar su código para obtener seguridad de tipo con alguna forma de constantes enumeradas, y consideraría no necesita color_assign() en primer lugar para que la seguridad de tipos sea un juego justo para la respuesta).

Los punteros se encuentran entre las pocas formas que C trata como de tipo seguro, por lo que son un candidato natural para resolver este problema. Así que lo atacaría de esta manera: en lugar de usar una enum , sacrificaría un poco de memoria para poder tener valores de puntero únicos y predecibles, y luego usaría algunas declaraciones #define realmente extravagantes para construir mi "enumeración" ( sí, sé que las macros contaminan el espacio de nombres macro, pero enum contamina el espacio de nombres global del compilador, por lo que lo considero cercano a un intercambio uniforme):

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

Esto, por supuesto, requiere que tenga suficiente memoria reservada en algún lugar para garantizar que nada más pueda asumir esos valores de puntero:

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

Pero desde la perspectiva del consumidor, todo esto está oculto: color_t es casi un objeto opaco. No puede asignarle nada más que valores válidos de color_t y NULL:

usuario.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 */ }

Esto funciona bien en la mayoría de los casos, pero tiene el problema de no funcionar en las declaraciones de switch ; no puedes switch un puntero (lo cual es una pena). Pero si está dispuesto a agregar una macro más para hacer posible el cambio, puede llegar a algo que es "lo suficientemente bueno":

color.h :

... #define COLOR_NUMBER(c) / ((c) - color_dummy_array)

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

¿Es esta una buena solución? No lo llamaría genial , ya que desperdicia algo de memoria y contamina el espacio de nombres macro, y no le permite usar enum para asignar automáticamente sus valores de color, pero es otra forma de resolver el problema que resulta en algo más usos naturales, y a diferencia de la respuesta principal, funciona desde C89.


Se podría hacer cumplir la seguridad de tipos con una 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 };

Dado que el color es solo un entero envuelto, se puede pasar por valor o por puntero como se haría con un int . Con esta definición de color , color_assign(&val, 3); falla al compilar con:

error: tipo incompatible para el argumento 2 de ''color_assign''

color_assign(&val, 3); ^

Ejemplo completo (de trabajo):

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

Juega en línea (demo) .