56 votes

Comment créer des enums sécurisés ?

La sécurité des types avec les enums en C est problématique, car il s'agit essentiellement 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 trucs avec des pointeurs comme ceci :

typedef enum
{
  BLUE,
  RED
} color_t;

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

Parce que les pointeurs ont des règles de type plus strictes que les valeurs, cela permet d'éviter un code tel que celui-ci :

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

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

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

Cela est dû au fait que la constante d'énumération est essentiellement juste une int et peut être implicitement assigné à une variable d'énumération.

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

0 votes

Jetez un coup d'œil à ma réponse ci-dessous : stackoverflow.com/questions/39725331/

0 votes

@shjeff C'est assez similaire à certaines des versions structurées postées ci-dessous.

55voto

Lundin Points 21616

Il est possible d'y parvenir grâce à quelques astuces. Étant donné

typedef enum
{
  BLUE,
  RED
} color_t;

Définissez ensuite une union fictive qui ne sera pas utilisée par l'appelant, mais qui contient des membres portant les mêmes noms que les constantes de l'é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.

Ensuite, créez 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 ensuite appelées de la manière suivante :

color_t color;
color_assign(color, BLUE); 

Explication :

  • Le C11 _Generic garantit que la variable d'énumération est du bon type. Cependant, il ne peut pas être utilisé sur la constante d'énumération BLUE car il est de type int .
  • Par conséquent, la macro d'aide c_assign crée une instance temporaire de l'union fictive, où la syntaxe de l'initialisateur désigné est utilisée pour attribuer la valeur BLUE à un membre du syndicat nommé BLUE . Si un tel membre n'existe pas, le code ne sera pas compilé.
  • Le membre de l'union du type correspondant est alors copié dans la variable enum.

En fait, nous n'avons pas besoin de la macro d'aide, je divise simplement l'expression pour plus de lisibilité. Il est tout aussi possible 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

EDIT

Évidemment, ce qui précède n'empêche pas l'appelant de simplement taper color = garbage; . Si vous souhaitez bloquer entièrement la possibilité d'utiliser une telle affectation de l'enum, vous pouvez le mettre dans un struct 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) \
  )

couleur.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);

0 votes

C'est vraiment mignon, même si ça ne rattrape pas certaines erreurs : int zonk(int x) {color_t color; color = x; return color;}

0 votes

@gsg Vous devrez évidemment interdire les affectations directes. Cela peut être réalisé, par exemple, en intégrant l'enum dans un struct et en faisant de ce struct un type opaque.

0 votes

@gsg J'ai ajouté un exemple avec une encapsulation privée qui bloque les affectations directes.

9voto

Sean Werkema Points 165

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

(Il faut admettre que la question portait explicitement sur cómo pour écrire color_assign() mais si vous regardez la question de manière plus large, il s'agit en fait de savoir comment modifier votre code pour obtenir la sécurité de type avec une certaine forme de constantes énumérées, et je considérerais qu'il n'est pas nécessaire d'utiliser l'option color_assign() en premier lieu pour que la sécurité de type soit un jeu équitable pour la réponse).

Les pointeurs font partie des quelques formes que le C traite comme étant de type sûr, ils constituent donc un candidat naturel pour résoudre ce problème. Je l'attaquerais donc de cette façon : Plutôt que d'utiliser un enum je sacrifierais un peu de mémoire pour pouvoir disposer de valeurs de pointeur uniques et prévisibles, puis j'utiliserais un système vraiment funky et bizarre. #define pour construire mon "enum" (oui, je sais que les macros polluent l'espace de nom des macros, mais enum pollue l'espace de noms global du compilateur, donc je considère que c'est presque un échange équitable) :

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 :

couleur.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 pratiquement un objet opaque. Vous ne pouvez pas lui attribuer quoi que ce soit d'autre que des données valides. color_t et NULL :

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

Cette méthode fonctionne bien dans la plupart des cas, mais elle a le problème de ne pas fonctionner dans les cas suivants switch déclarations ; vous ne pouvez pas switch sur un pointeur (ce qui est dommage). Mais si vous êtes prêt à ajouter une macro de plus pour rendre la commutation possible, vous pouvez arriver à quelque chose qui est "suffisamment bon" :

color.h :

...

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

utilisateur.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 que c'est un bon solution ? Je ne l'appellerais pas grand car elle gaspille de la mémoire et pollue l'espace de noms des macros, et elle ne vous permet pas d'utiliser la fonction enum pour attribuer automatiquement vos valeurs de couleur, mais il es une autre façon de résoudre le problème qui donne lieu à des utilisations un peu plus naturelles, et contrairement à la première réponse, elle fonctionne jusqu'en C89.

1 votes

L'utilisation des fonctionnalités du C11 n'est pas un inconvénient légitime.

10 votes

C'est un inconvénient si votre compilateur ne supporte pas les fonctionnalités de la C11. Je ne citerai pas de noms ( toux Microsoft toux ) mais il existe un certain nombre de compilateurs "C" qui ne peuvent pas gérer le C11.

1 votes

Une idée intéressante. Vous devriez cependant envisager de cacher entièrement la définition de la structure, afin que personne n'ait l'idée de l'utiliser ou d'accéder à ses membres. Cela peut être fait avec des types opaques, comme le montre l'édition dans ma réponse. En outre, si vous déclarez la structure const vous ne gaspillez pas d'espace dans les .bss mais plutôt dans les .rodata ou quelque chose comme ça. Et qu'en est-il de color_t color = 0; ? Ou pire : toute expression qui donne la valeur 0 au moment de la compilation.

7voto

YSC Points 3386

On pourrait renforcer la sécurité des types 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  };

Depuis color est juste un entier enveloppé, il peut être transmis par valeur ou par pointeur comme on le ferait avec un fichier de type int . Avec cette définition de color , color_assign(&val, 3); ne parvient pas à compiler avec :

error : incompatible type for argument 2 of 'color_assign' (erreur)

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

Jouer avec en ligne (démo) .

0 votes

Je crois que cela vous oblige à utiliser des noms différents pour les enums et les const structs.

0 votes

@Lundin C'est le cas : chaque couleur reçoit un privé (ou interne ) nom ( enum ) et un public un ( const struct ). Je ne le considère pas comme un inconvénient.

7voto

Graham Points 461

En définitive, 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 le faire. Cependant, vous pouvez facilement utiliser un outil d'analyse statique pour détecter ce problème - Clang est l'outil gratuit le plus évident, mais il en existe beaucoup d'autres. Indépendamment du fait que le langage soit sûr du point de vue du type, l'analyse statique peut détecter et signaler le problème. En général, un outil d'analyse statique affiche des avertissements et non des erreurs, mais vous pouvez facilement faire en sorte que l'outil d'analyse statique signale une erreur au lieu d'un avertissement, et modifier votre fichier makefile ou votre projet de construction pour gérer cela.

1 votes

Il est évident que les analyseurs statiques sont toujours une option, par exemple un vérificateur MISRA-C:2012 détecterait les problèmes de type enum. Le principal problème de tous les analyseurs statiques du marché est qu'ils sont tellement remplis de bogues/"faux positifs" qu'ils ne sont pas très utiles. Si vous pouvez forcer un diagnostic du compilateur par n'importe quel compilateur C standard, c'est toujours la solution préférée.

2 votes

@Lundin Mon expérience de l'analyse statique n'est pas qu'elle est pleine de bogues, mais que le C idiomatique enfreint fréquemment les normes de codage - "if(ptr)" comme vérification de non-NULL, par exemple. Une grande partie de l'effort de l'analyse statique doit être consacrée à affiner votre jeu de règles. En revanche, une fois que vous l'avez fait, vous disposez d'un outil très puissant qui améliorera réellement votre code.

0 votes

@Lundin L'ajout de fonctions et de macros redondantes dans le code semble augmenter la complexité, ce qui réduit finalement la qualité du code. Le temps passé à implémenter et retravailler le code précédent est, à mon avis, mieux utilisé avec les outils d'analyse statique.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X