Wednesday, November 17, 2010

Serialisable Enums in C++ Using Templates and the Preprocessor

I have been too busy at work lately to spend any significant time on Battle Balloons. On the upside, it has been very interesting late-night work delving deep into console graphics (both Microsoft Xbox 360 and Sony PlayStation 3)!

Anyway, here's a little bit of unrelated C++ hackery I use in Battle Ballons code. It allows you to declare an enum once (even within a class) and have it automatically convertible to/from strings - all without any auto-generation scripts. I use it to allow my config files (specified in YAML) to reference C++ enums defined in code. Super-useful since Battle Balloons is heavily data-driven!

Common implementations out there seem to use code-generators or multiple #includes of the same file. This is similar to the latter, except I pass in the list of enums twice (automated for you through a small macro).

The template class is not entirely necessary - you could easily embed the same code into the macro. But the big advantage the template has here is that you can actually step through the code in a debugger (at least in Visual Studio you can)!

More details in the code snippets below. It is rough and references project-specific code not shown, but the general concept is there and should be possible to reproduce.

First, a quick example on how it is used:
ENUM_CLASS(TestEnum, TE_A, TE_B = 2, TE_C, TE_D = 6);

void testEnum
{
    ASSERT(TE_A == 0);
    ASSERT(TE_B == 2);
    ASSERT(TE_C == 3);
    ASSERT(TE_D == 6);
    ASSERT(MAX_TestEnum == 7);

    TestEnum te;
    ASSERT(te == TestEnum::max());
    ASSERT(te == MAX_TestEnum);

    te.setFromString("TE_A");
    ASSERT(te == TE_A);

    te.setFromString("TE_B");
    ASSERT(te == TE_B);

    ASSERT(TestEnum::getEnum("TE_C") == TE_C);
    ASSERT(TestEnum::getString(TE_D) == "TE_D");
}

Header file (.h):
#define ENUM_CLASS(Name, ...)   enum E##Name { __VA_ARGS__, MAX_##Name };                           \
                                struct Get##Name##EnumString                                        \
                                {                                                                   \
                                    static const Char *get()                                        \
                                    {                                                               \
                                        return #__VA_ARGS__ ", MAX_" #Name;                         \
                                    }                                                               \
                                };                                                                  \
                                typedef EnumT Name

void tokeniseEnumString(String *enumArray, Size enumArrayCapacity, const String &enumString);

template <class Enum, const Size MAX_ENUM, class GetEnumString>
class EnumT : public EnumBase
{
public:
    static const String &getString(Size i)
    {
        ASSERT(i <= MAX_ENUM);
        return enumNames()[i];
    }

    static Enum getEnum(const String &s)
    {
        for (Size i = 0; i < MAX_ENUM; ++i)
        {
            if (enumNames()[i] == s)
                return static_cast(i);
        }
        return static_cast<Enum>(MAX_ENUM);
    }

    static Size max() { return MAX_ENUM; }

    EnumT() : m_value(static_cast<Enum>(MAX_ENUM)) {}
    EnumT(Size value) : m_value(static_cast<Enum>(value)) {}
    EnumT(Enum value) : m_value(value) {}

    EnumT &operator=(Enum e) { m_value = e; return *this; }
    operator Enum() const { return m_value; } 

    virtual bool setFromString(const String &s) { m_value = getEnum(s); return m_value != MAX_ENUM; }
    const Char *str() const { return *getString(m_value); }

private:
    static String *enumNames()
    {
        static bool s_init = false;
        static String s_enumNames[MAX_ENUM + 1];
        if (!s_init)
        {
            s_init = true;
            tokeniseEnumString(s_enumNames, MAX_ENUM + 1, GetEnumString::get());
        }
        return s_enumNames;
    }

    Enum m_value;
};

Implementation file (.cpp), which is is just the tokenising function that splits the enum declaration string (passed by the ENUM_CLASS macro function) into its individual names (and also respecting explicit number values, if given). This is only called once (on first use) and the result is cached off for all future calls:
void tokeniseEnumString(String *enumArray, Size enumArrayCapacity, const String &enumString)
{
    Size enumArraySize = 0;

    Size cIndex = 0;
    while(cIndex < enumString.len())
    {
        String name;
        bool hasExplicitValue = false;

        for (; cIndex < enumString.len(); ++cIndex)
        {
            const Char nameChar = enumString[cIndex];

            if (nameChar == ',')
            {
                ++cIndex;
                break;
            }

            if (nameChar == '=')
            {
                hasExplicitValue = true;
                ++cIndex;
                break;
            }

            if (std::isspace(nameChar))
                continue;

            name += nameChar;
        }

        if (hasExplicitValue)
        {
            String numberStr;

            for (; cIndex < enumString.len(); ++cIndex)
            {
                const Char numberChar = enumString[cIndex];

                if (std::isspace(numberChar))
                    continue;

                if (numberChar == ',')
                {
                    ++cIndex;
                    break;
                }

                ASSERT(std::isdigit(numberChar));
                numberStr += numberChar;
            }

            Int intValue;
            ASSERT(numberStr.toInt(intValue));
            ASSERT(intValue >= 0);

            const Size value = static_cast(intValue);
            ASSERT(value >= enumArraySize);
            while (enumArraySize < value)
                ++enumArraySize;
        }

        enumArray[enumArraySize] = name;
        ++enumArraySize;
    }

    ASSERT(enumArraySize == enumArrayCapacity);
}