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ónBLUE
porque es de tipoint
. -
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 valorBLUE
a un miembro de la unión llamadoBLUE
. 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"
}