Search code examples
cenumstype-safety

How to create type safe enums?


To achieve type safety with enums in C is problematic, since they are essentially just integers. And enumeration constants are in fact defined to be of type int by the standard.

To achieve a bit of type safety I do tricks with pointers like this:

typedef enum
{
  BLUE,
  RED
} color_t;

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

Because pointers have stricter type rules than values, so this prevents code such as this:

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

But it doesn't prevent code like this:

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

This is because the enumeration constant is essentially just an int and can get implicitly assigned to an enumeration variable.

Is there a way to write such a function or macro color_assign, that can achieve complete type safety even for enumeration constants?


Solution

  • It is possible to achieve this with a few tricks. Given

    typedef enum
    {
      BLUE,
      RED
    } color_t;
    

    Then define a dummy union which won't be used by the caller, but contains members with the same names as the enumeration constants:

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

    This is possible because enumeration constants and member/variable names reside in different namespaces.

    Then make some function-like macros:

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

    These macros are then called like this:

    color_t color;
    color_assign(color, BLUE); 
    

    Explanation:

    • The C11 _Generic keyword ensures that the enumeration variable is of the correct type. However, this can't be used on the enumeration constant BLUE because it is of type int.
    • Therefore the helper macro c_assign creates a temporary instance of the dummy union, where the designated initializer syntax is used to assign the value BLUE to a union member named BLUE. If no such member exists, the code won't compile.
    • The union member of the corresponding type is then copied into the enum variable.

    We actually don't need the helper macro, I just split the expression for readability. It works just as fine to write

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

    Examples:

    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

    Obviously the above doesn't prevent the caller from simply typing color = garbage;. If you wish to entirely block the possibility of using such assignment of the enum, you can put it in a struct and use the standard procedure of private encapsulation with "opaque type":

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

    main.c

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