The default implementation of enums leaves much to be desired, especially when it comes to operating on their text representation.
Parting or printing an enum usually involves extra, hand-written functions for each which also have to be maintained along with
the main enum.
Fortunately, X Lists can fix this. Declare all of your enums as X Lists instead and you get a bunch of extra featuers for free.
#define COLOR_LIST(X) \
X(Red) \
X(Green) \
X(Blue)
Then the enum itself:
enum {
#define X(a, ...) COLOR_##a,
COLOR_LIST(X)
#undef X
COLOR_MAX_VALUE
};
Note that the extra entry on the end give you the number of enum values that exist.
And an array of the names:
// in the header
extern char const * const COLOR__names[];
// in a source file
char const * const COLOR__names[] = {
#define X(a, ...) [COLOR_##a] = #a,
COLOR_LIST(X)
#undef X
NULL
};
Now you can trivially iterate all the enum names:
for(long i = 0; i < COLOR_MAX_VALUE; i++) {
printf("%s = %ld\n", COLOR__names[i], i);
}
And you can look up an enum value by name:
long COLOR__getValue(char* name) {
for(long i = 0; i < COLOR_MAX_VALUE; i++) {
if(!strcmp(COLOR__names[i], name)) return i;
}
return -1;
}
But what if my enum has explicitly set, non-contiguous, or negative values? Not a problem:
// in a header file
#define COLOR_LIST(X) \
X(Red, -5) \
X(Green, 7) \
X(Blue, 0xdeadbeef)
enum {
#define X(a, v, ...) COLOR_ORDINAL_##a,
COLOR_LIST(X)
#undef X
COLOR_MAX_VALUE
};
typedef enum : long {
#define X(a, v, ...) COLOR_##a = v,
COLOR_LIST(X)
#undef X
} color_t;
typedef struct {
color_t value;
char const * const name;
} enum_name_pair;
extern enum_name_pair const COLOR__nameLookup[];
// in a source file
enum_name_pair const COLOR__nameLookup[] = {
#define X(a, v, ...) [COLOR_ORDINAL_##a] = {v, #a},
COLOR_LIST(X)
#undef X
{0, NULL}
};
long COLOR__getValue(char* name) {
for(long i = 0; i < COLOR_MAX_VALUE; i++) {
if(!strcmp(COLOR__nameLookup[i].name, name)) return COLOR__nameLookup[i].value;
}
return -1;
}
Which leads us to bit flags:
// in a header
#define COLOR_LIST(X) \
X(Red) \
X(Green) \
X(Blue)
enum {
#define X(a, v, ...) COLOR_ORDINAL_##a,
COLOR_LIST(X)
#undef X
COLOR_MAX_VALUE
};
enum {
#define X(a, v, ...) COLOR_BIT_##a = 1ul << COLOR_ORDINAL_##a,
COLOR_LIST(X)
#undef X
};
X Lists don't have to exclusively use X as the internal value:
#define MENU_OPTION_LIST(X, Y) \
X(Play) \
X(Load) \
X(Settings) \
Y(DebugOptions) \
Y(LevelEditor)
#if DEBUG
#define Y(...) X(__VA_ARGS__)
#else
#define Y(...)
#endif
enum {
#define X(a, ...) MENU_OPTION_##a,
MENU_OPTION_LIST(X, Y)
#undef X
MENU_OPTION_MAX_VALUE
};
You can include arbitrary metadata along with the enum values:
// in a header
#define MENU_OPTION_LIST(X, Y) \
X(Play, "Start a new game") \
X(Load, "Resume a saved game") \
X(Settings, "Graphics and audio settings") \
Y(DebugOptions, "Developer tools") \
Y(LevelEditor, "Level designer")
#if DEBUG
#define Y(...) X(__VA_ARGS__)
#else
#define Y(...)
#endif
enum {
#define X(a, ...) MENU_OPTION_##a,
MENU_OPTION_LIST(X, Y)
#undef X
MENU_OPTION_MAX_VALUE
};
extern char const * const MENU_OPTION__descriptions[];
// in a source file
char const * const MENU_OPTION__descriptions[] = {
#define X(a, d, ...) [COLOR_##a] = d,
MENU_OPTION_LIST(X, Y)
#undef X
NULL
};
Be sure to make your X macros variadic so that you don't have to go back and update them all when you add more metadata fields.
You can of course abstract some of this code with more macros but the examples here are written with clarity in mind. One of the important aspects
is consistency in naming conventions. It doesn't matter as much what your conventions are so long as you don't have to think about it or
look it up when you need to use it. After some playing around, I've settled on the following:
- Enums: PREFIX_NAME_EntryName, with the prefix in all-caps SNAKE_CASE with one underscore connecting to the individual item name
in CapitalizedWords with no underscores.
- The terminating value for the number of enum entries is PREFIX_NAME_MAX_VALUE.
- A flat array of char* names is PREFIX_NAME__names, with two middle underscores to prevent name conflicts.
- A flat array of metadata structs names is PREFIX_NAME__metadataName, with two middle underscores to prevent name conflicts.
- Ordinal enums used for sparse enums or bit flags are PREFIX_NAME_ORD__EntryName.
- The enum collection itself is typedefed as prefix_name_t, in all-lower snake_case with _t suffix.
- The X List for the enum is named PREFIX_NAME_LIST(X), with X usually passed as an argument.
Trivial Enums
Enums are great but you shouldn't take them to ridiculous extremes. Consider the case of storing the type of file entry in a directory listing.
You could make some enum like:
enum : uint8_t {
FILE_TYPE_Directory,
FILE_TYPE_RegularFile,
FILE_TYPE_Symlink,
FILE_TYPE_DeviceNode
};
Or you could
simply use a single char with the ascii values 'd', 'f', 's', 'n' used as character literals with a simple comment in the struct.
Don't overcomplicate what doesn't need to be complicated.