web-dev-qa-db-fra.com

Comment créer des énumérations sécurisées de type?

Atteindre la sécurité de type avec des énumérations en C est problématique, car il ne s'agit essentiellement que d'entiers. Et les constantes d'énumération sont en fait définies comme étant de type int par la norme.

Pour obtenir un peu de sécurité de type, je fais des tours avec des pointeurs comme celui-ci:

typedef enum
{
  BLUE,
  RED
} color_t;

void color_assign (color_t* var, color_t val) 
{ 
  *var = val; 
}

Étant donné que les pointeurs ont des règles de type plus strictes que les valeurs, cela empêche le code tel que celui-ci:

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

Mais cela n'empêche pas un code comme celui-ci:

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

En effet, la constante d'énumération est essentiellement un int et peut être implicitement affectée à une variable d'énumération.

Existe-t-il un moyen d'écrire une telle fonction ou macro color_assign, qui peut atteindre une sécurité de type complète même pour les constantes d'énumération?

54
Lundin

Il est possible d'y parvenir avec quelques astuces. Donné

typedef enum
{
  BLUE,
  RED
} color_t;

Définissez ensuite une union factice qui ne sera pas utilisée par l'appelant, mais qui contient des membres portant les mêmes noms que les constantes d'énumération:

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

Cela est possible car les constantes d'énumération et les noms de membres/variables résident dans des espaces de noms différents.

Créez ensuite des macros de type fonction:

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

Ces macros sont alors appelées comme ceci:

color_t color;
color_assign(color, BLUE); 

Explication:

  • Le C11 _Generic Le mot clé garantit que la variable d'énumération est du type correct. Cependant, cela ne peut pas être utilisé sur la constante d'énumération BLUE car elle est de type int.
  • Par conséquent, la macro d'assistance c_assign crée une instance temporaire de l'union factice, dans laquelle la syntaxe d'initialisation désignée est utilisée pour affecter la valeur BLUE à un membre de l'union nommé BLUE. Si aucun membre de ce type n'existe, le code ne sera pas compilé.
  • Le membre d'union du type correspondant est ensuite copié dans la variable enum.

Nous n'avons en fait pas besoin de la macro d'aide, je viens de diviser l'expression pour plus de lisibilité. Ça marche aussi bien d'écrire

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

Exemples:

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

ÉDITER

Évidemment, ce qui précède n'empêche pas l'appelant de simplement taper color = garbage;. Si vous souhaitez bloquer complètement la possibilité d'utiliser une telle affectation de l'énumération, vous pouvez le mettre dans une structure et utiliser la procédure standard d'encapsulation privée avec "type 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) \
  )

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

principal c

col_t* color;
color = col_alloc();

color_assign(color, BLUE); 

col_free(color);
51
Lundin

La bonne réponse est plutôt bonne, mais elle a les inconvénients de nécessiter une grande partie des fonctionnalités C99 et C11 pour être compilées, et en plus, cela rend l'affectation assez artificielle: vous devez utiliser une fonction magique color_assign() fonction ou macro pour déplacer les données au lieu de l'opérateur standard =.

(Certes, la question demandait explicitement comment écrire color_assign(), mais si vous regardez la question plus largement, il s'agit vraiment de comment changer votre code pour obtenir la sécurité de type avec une certaine forme de constantes énumérées, et je considérerais de ne pas avoir besoin de color_assign() en premier lieu pour que la sécurité de type soit un jeu équitable pour la réponse.)

Les pointeurs sont parmi les quelques formes que C considère comme sécuritaires, ils constituent donc un candidat naturel pour résoudre ce problème. Donc, je l'attaquerais de cette façon: Plutôt que d'utiliser un enum, je sacrifierais un peu de mémoire pour pouvoir avoir des valeurs de pointeur uniques et prévisibles, puis j'utiliserais du funky vraiment hokey #define pour construire mon "enum" (oui, je sais que les macros polluent l'espace de noms des macros, mais enum pollue l'espace de noms global du compilateur, donc je le considère proche d'un commerce pair):

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

Cela nécessite, bien sûr, que vous ayez juste assez de mémoire réservée quelque part pour vous assurer que rien d'autre ne pourra jamais prendre ces valeurs de pointeur:

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

Mais du point de vue du consommateur, tout cela est caché: color_t Est presque un objet opaque. Vous ne pouvez pas lui attribuer autre chose que des valeurs color_t Valides et NULL:

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

Cela fonctionne bien dans la plupart des cas, mais il a le problème de ne pas fonctionner dans les instructions switch; vous ne pouvez pas switch sur un pointeur (ce qui est dommage). Mais si vous êtes prêt à ajouter une macro supplémentaire pour rendre la commutation possible, vous pouvez arriver à quelque chose de "assez bon":

color.h:

...

#define COLOR_NUMBER(c) \
    ((c) - color_dummy_array)

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

Est-ce une bonne solution? Je ne l'appellerais pas génial , car il gaspille de la mémoire et pollue l'espace de noms des macros, et il ne vous permet pas d'utiliser enum pour attribuer automatiquement vos valeurs de couleur, mais c'est une autre façon de résoudre le problème qui se traduit par des utilisations un peu plus naturelles, et contrairement à la réponse du haut, cela fonctionne tout le chemin du retour à C89.

8
Sean Werkema

En fin de compte, ce que vous voulez, c'est un avertissement ou une erreur lorsque vous utilisez une valeur d'énumération non valide.

Comme vous le dites, le langage C ne peut pas faire cela. Cependant, vous pouvez facilement utiliser un outil d'analyse statique pour détecter ce problème - Clang est le plus évident, mais il y en a beaucoup d'autres. Que la langue soit de type sécurisé ou non, l'analyse statique peut détecter et signaler le problème. En règle générale, un outil d'analyse statique affiche des avertissements, pas des erreurs, mais vous pouvez facilement demander à l'outil d'analyse statique de signaler une erreur au lieu d'un avertissement et de modifier votre makefile ou votre projet de génération pour gérer cela.

7
Graham

On pourrait appliquer la sécurité de type avec un 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  };

Puisque color n'est qu'un entier encapsulé, il peut être passé par valeur ou par pointeur comme on le ferait avec un int. Avec cette définition de color, color_assign(&val, 3); ne parvient pas à compiler avec:

erreur: type incompatible pour l'argument 2 de 'color_assign'

     color_assign(&val, 3);
                        ^

Exemple complet (fonctionnel):

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

Jouez avec en ligne (démo) .

7
YSC