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 anstd::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?