Background
Enums are used in game programming to represent many different things – for example the states of a character, or the possible directions of motion:
enum State {Idle, Fidget, Walk, Scan, Attack}; enum Direction {North, South, East, West};
During debugging, it would be useful to see "State: Fidget" printed in the debug console instead of a number, as in "State: 1". You might also need to serialize enums to JSON, YAML, or another format, and might prefer strings to numbers. Besides making the output more readable to humans, using strings in the serialization format makes it resistant to changes in the numeric values of the enum constants. Ideally, "Fidget" should still map to Fidget, even if new constants are declared and Fidget ends up having a different value than 1.
Unfortunately, C++ enums don't provide an easy way of converting their values to (and from) string. So, developers have had to resort to solutions that are either difficult to maintain, such as hard-coded conversions, or that have restrictive and unappealing syntax, such as X macros. Sometimes, developers have also chosen to use additional build tools to generate the necessary conversions automatically. Of course, this complicates the build process. Enums meant for input to these build tools usually have their own syntax and live in their own input files. The build tools require special handling in the Makefile or project files.
Pure C++ solution
It turns out to be possible to avoid all the above complications and generate fully reflective enums in pure C++. The declarations look like this:
BETTER_ENUM(State, int, Idle, Fidget, Walk, Scan, Attack) BETTER_ENUM(Direction, int, North, South, East, West)
And can be used as:
State state = State::Fidget; state._to_string(); // "Fidget" std::cout << "state: " << state; // Writes "state: Fidget" state = State::_from_string("Scan"); // State::Scan (3) // Usable in switch like a normal enum. switch (state) { case State::Idle: // ... break; // ... }
This is done using a few preprocessor and template tricks, which will be sketched out in the last part of the article.
Besides string conversions and stream I/O, it is also possible to iterate over the generated enums:
for (Direction direction : Direction._values()) character.try_moving_in_direction(direction);
You can generate enums with sparse ranges and then easily count them:
BETTER_ENUM(Flags, char, Allocated = 1, InUse = 2, Visited = 4, Unreachable = 8) Flags::_size(); // 4
If you are using C++11, you can even generate code based on the enums, because all the conversions and loops can be run at compile time using constexpr functions. It is easy, for example, to write a constexpr function that will compute the maximum value of an enum and make it available at compile time – even if the constants have arbitrary values and are not declared in increasing order.
I have packed the implementation of the macro into a library called Better Enums, which is available on GitHub. It is distributed under the BSD license, so you can do pretty much anything you want with it for free. The implementation consists of a single header file, so using it is as simple as adding enum.h to your project directory. Try it out and see if it solves your enum needs.
How it works
To convert between enum values and strings, it is necessary to generate a mapping between them. Better Enums does this by generating two arrays at compile time. For example, if you have this declaration:
BETTER_ENUM(Direction, int, North = 1, South = 2, East = 4, West = 8)
The macro will expand to something like this:
struct Direction { enum _Enum {North = 1, South = 2, East = 4, West = 8}; static const int _values[] = {1, 2, 4, 8}; static const char * const _names[] = {"North", "South", "East", "West"}; // ...functions using the above declarations... };
Then, it's straightforward to do the conversions: look up the index of the value or string in _values or _names, and return the corresponding value or string in the other array. So, the question is how to generate the arrays.
The values array
The _values array is generated by referring to the constants of the internal enum _Enum. That part of the macro looks like this:
static const int _values[] = {__VA_ARGS__};
which expands to
static const int _values[] = {North = 1, South = 2, East = 4, West = 8};
This is almost a valid array declaration. The problem is the extra initializers such as "= 1". To deal with these, Better Enums defines a helper type whose purpose is to have an assignment operator, but ignore the value being assigned:
template <typename T> struct _eat { T _value; template <typename Any> _eat& operator =(Any value) { return *this; } // Ignores its argument. explicit _eat(T value) : _value(value) { } // Convert from T. operator T() const { return _value; } // Convert to T. }
It is then possible to turn the initializers "= 1" into assignment expressions that have no effect:
static const int _values[] = {(_eat<_Enum>)North = 1, (_eat<_Enum>)South = 2, (_eat<_Enum>)East = 4, (_eat<_Enum>)West = 8};
The strings array
For the strings array, Better Enums uses the preprocessor stringization operator (#), which expands __VA_ARGS__ to something like this:
static const char * const _names[] = {"North = 1", "South = 2", "East = 4", "West = 8"};
We almost have the constant names as strings – we just need to trim off the initializers. Better Enums doesn't actually do that, however. It simply treats the whitespace characters and the equals sign as additional string terminators when doing comparisons against strings in the _names array. So, when looking at "North = 1", Better Enums sees only "North".
Is it possible to do without a macro?
I don't believe so, for the reason that stringization (#) is the only way to convert a source code token to a string in pure C++. One top-level macro is therefore the minimum amount of macro overhead for any reflective enum library that generates conversions automatically.
Other considerations
The full macro implementation is, of course, somewhat more tedious and complicated than what is sketched out in this article. The complications arise mostly from supporting constexpr usage, dealing with static arrays, accounting for the quirks of various compilers, and factoring as much of the macro as possible out into a template for better compilation speed (templates don't need to be re-parsed when instantiated, but macro expansions do).