8
\$\begingroup\$

I recently had the need of "reflecting" multiple enum class constructs in order to get their elements' names as std::string objects or their element count. I came up with a C++11 variadic macro solution:

namespace ssvu
{
    namespace Internal
    {
        inline std::vector<std::string> getSplittedEnumVarArgs(const std::string& mEnumVarArgs)
        {
            std::vector<std::string> result;

            // getSplit returns a collection of substrings split at a certain token
            // Example: "a,b,c" -> {"a", "b", "c"}

            // getTrimmedStrLR removes whitespace from the beginning and the end of a string
            // Example: "  abc " -> "abc"

            for(const auto& s : getSplit(mEnumVarArgs, ',')) 
                result.emplace_back(getTrimmedStrLR(std::string(std::begin(s), find(s, '='))));

            return result;
        }

        template<typename> struct ReflectedEnumImpl;

        template<template<typename> class T, typename TEnum> struct ReflectedEnumImpl<T<TEnum>>
        {
            using EnumType = T<TEnum>;
            inline static const std::vector<std::string>& getElementsAsStrings() noexcept
            {
                static std::vector<std::string> result(getSplittedEnumVarArgs(EnumType::getEnumString()));
                return result;
            }
            inline static std::size_t getElementCount() noexcept
            {
                return getElementsAsStrings().size();
            }
            inline static const std::string& getElementAsString(TEnum mElement) noexcept
            {
                // If the user changed the default enum values by using the `= ...'
                // syntax, this function will return wrong values and possibly
                // go out of bounds. Maybe this should throw an exception.
                assert(!contains(EnumType::getEnumString(), '='));

                return getElementsAsStrings()[std::size_t(mElement)];
            }
        };
    }

    #define SSVU_REFLECTED_ENUM_DEFINE_MANAGER(mName) template<typename> class mName

    #define SSVU_REFLECTED_ENUM(mManagerName, mName, mUnderlying, ...) enum class mName : mUnderlying { __VA_ARGS__ }; \
        template<> class mManagerName<mName> : public ssvu::Internal::ReflectedEnumImpl<mManagerName<mName>> \
        { \
            friend ssvu::Internal::ReflectedEnumImpl<mManagerName<mName>>; \
            inline static const std::string& getEnumString(){ static std::string result{#__VA_ARGS__}; return result; } \
        }
}

Example usage:

SSVU_REFLECTED_ENUM_DEFINE_MANAGER(ReflectedEnum);
SSVU_REFLECTED_ENUM(ReflectedEnum, Colors, int, Red, Yellow, Green);

void tests()
{
    assert(int(Colors::Red) == 0);
    assert(int(Colors::Yellow) == 1);
    assert(int(Colors::Green) == 2);

    assert(ReflectedEnum<Colors>::getElementAsString(Colors::Red) == "Red");
    assert(ReflectedEnum<Colors>::getElementAsString(Colors::Yellow) == "Yellow");
    assert(ReflectedEnum<Colors>::getElementAsString(Colors::Green) == "Green");
}

What do you think?

Thoughts/questions:

  • Consider the case where the user defines custom values for the enum elements:

    SSVU_REFLECTED_ENUM(ReflectedEnum, Test, int, A = -2, B = 15, C = 0);
    

    Getting element count would still be possible, as it's easy to count variadic macro elements. However, getting an element's name as a string would require using a std::map instead of an array. Should I figure out a way to detect if the enum has custom values? Or should I ditch the array for an std::map altogheter?

    Or would an alternative syntax be better? Example:

    SSVU_REFLECTED_CUSTOM_ENUM(ReflectedEnum, Test, int, A, -2, B, 15, C, 0);
    

    Maybe this would be more flexible and easier to work with.

  • I have macro variadic args iteration facilities in my ssvu library. Do you think it's worthwhile figuring out a way to generate the enum string elements array at compile-time with macro metaprogramming? Or is the current solution good enough?

\$\endgroup\$
9
\$\begingroup\$

Just a few things to point out:

  • There's no need to use inline yourself. For modern compilers, it merely serves as a suggestion, but they can otherwise determine if and when it's really needed. Read this for more info.

  • You should use consistent naming for your namespaces (one is lowercase and the other is uppercase). I'd not use uppercase as it's commonly used for user-defined types.

  • getElementCount(), like the other accessors here, should also return const.

\$\endgroup\$
  • 1
    \$\begingroup\$ I would even recommand to use the name details instead of Internal. It is commonly used to hide implementation details. \$\endgroup\$ – Morwenn Apr 4 '14 at 8:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.