• 11/20/15 01:02 AM
    Sign in to follow this  

    Maintenance-free Enum to String in Pure C++ with "Better Enums"

    Engines and Middleware

    antron
    • Posted By antron

    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: [code] enum State {Idle, Fidget, Walk, Scan, Attack}; enum Direction {North, South, East, West}; [/code] During debugging, it would be useful to see [tt]"State: Fidget"[/tt] printed in the debug console instead of a number, as in [tt]"State: 1"[/tt]. 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, [tt]"Fidget"[/tt] should still map to [tt]Fidget[/tt], even if new constants are declared and [tt]Fidget[/tt] ends up having a different value than [tt]1[/tt]. 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: [code] BETTER_ENUM(State, int, Idle, Fidget, Walk, Scan, Attack) BETTER_ENUM(Direction, int, North, South, East, West) [/code] And can be used as: [code] 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; // ... } [/code] 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: [code] for (Direction direction : Direction._values()) character.try_moving_in_direction(direction); [/code] You can generate enums with sparse ranges and then easily count them: [code] BETTER_ENUM(Flags, char, Allocated = 1, InUse = 2, Visited = 4, Unreachable = 8) Flags::_size(); // 4 [/code] 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 [tt]constexpr[/tt] functions. It is easy, for example, to write a [tt]constexpr[/tt] 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 [tt]enum.h[/tt] 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: [code] BETTER_ENUM(Direction, int, North = 1, South = 2, East = 4, West = 8) [/code] The macro will expand to something like this: [code] struct Direction { enum _Enum : int {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"}; int _value; // ...functions using the above declarations... }; [/code] Then, it's straightforward to do the conversions: look up the index of the value or string in [tt]_values[/tt] or [tt]_names[/tt], and return the corresponding value or string in the other array. So, the question is how to generate the arrays.

    The values array

    The [tt]_values[/tt] array is generated by referring to the constants of the internal enum [tt]_Enum[/tt]. That part of the macro looks like this: [code] static const int _values[] = {__VA_ARGS__}; [/code] which expands to [code] static const int _values[] = {North = 1, South = 2, East = 4, West = 8}; [/code] This is almost a valid array declaration. The problem is the extra initializers such as "[tt]= 1[/tt]". To deal with these, Better Enums defines a helper type whose purpose is to have an assignment operator, but ignore the value being assigned: [code] template struct _eat { T _value; template _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. } [/code] It is then possible to turn the initializers "[tt]= 1[/tt]" into assignment expressions that have no effect: [code] static const int _values[] = {(_eat<_Enum>)North = 1, (_eat<_Enum>)South = 2, (_eat<_Enum>)East = 4, (_eat<_Enum>)West = 8}; [/code]

    The strings array

    For the strings array, Better Enums uses the preprocessor stringization operator ([tt]#[/tt]), which expands [tt]__VA_ARGS__[/tt] to something like this: [code] static const char * const _names[] = {"North = 1", "South = 2", "East = 4", "West = 8"}; [/code] 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 [tt]_names[/tt] array. So, when looking at [tt]"North = 1"[/tt], Better Enums sees only [tt]"North"[/tt].

    Is it possible to do without a macro?

    I don't believe so, for the reason that stringization ([tt]#[/tt]) 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 [tt]constexpr[/tt] usage, dealing with [tt]static[/tt] 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). Nov. 22 2015: Clarified generated struct pseudocode to show that the size of the enum is set.


      Report Article
    Sign in to follow this  


    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now


    AMoreira

    Report ·

      

    Share this review


    Link to review
    Jan2go

    Report ·

      

    Share this review


    Link to review
    JVG_BCN

    Report ·

      

    Share this review


    Link to review
    Henderson

    Report ·

      

    Share this review


    Link to review
    Eck

    Report ·

      

    Share this review


    Link to review
    alh420

    Report ·

      

    Share this review


    Link to review
    xexuxjy

    Report ·

      

    Share this review


    Link to review
    mousetail

    Report ·

      

    Share this review


    Link to review
    Mussi

    Report ·

      

    Share this review


    Link to review
    swiftcoder

    Report ·

      

    Share this review


    Link to review
    FRex

    Report ·

      

    Share this review


    Link to review
    behc

    Report ·

      

    Share this review


    Link to review
    tookie

    Report ·

      

    Share this review


    Link to review
    Madhed

    Report ·

      

    Share this review


    Link to review
    Randy Gaul

    Report ·

      

    Share this review


    Link to review
    Dave Hunt

    Report ·

      

    Share this review


    Link to review
    Tesshu

    Report ·

      

    Share this review


    Link to review
    AthosVG

    Report ·

      

    Share this review


    Link to review