As long as you are okay with writing a separate .h/.cpp
pair for each queryable enum, this solution works with nearly the same syntax and capabilities as a regular c++ enum:
// MyEnum.h
#include <EnumTraits.h>
#ifndef ENUM_INCLUDE_MULTI
#pragma once
#end if
enum MyEnum : int ETRAITS
{
EDECL(AAA) = -8,
EDECL(BBB) = '8',
EDECL(CCC) = AAA + BBB
};
The .cpp
file is 3 lines of boilerplate:
// MyEnum.cpp
#define ENUM_DEFINE MyEnum
#define ENUM_INCLUDE <MyEnum.h>
#include <EnumTraits.inl>
Example usage:
for (MyEnum value : EnumTraits<MyEnum>::GetValues())
std::cout << EnumTraits<MyEnum>::GetName(value) << std::endl;
This solution requires 2 source files:
// EnumTraits.h
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#define ETRAITS
#define EDECL(x) x
template <class ENUM>
class EnumTraits
{
public:
static const std::vector<ENUM>& GetValues()
{
return values;
}
static ENUM GetValue(const char* name)
{
auto match = valueMap.find(name);
return (match == valueMap.end() ? ENUM() : match->second);
}
static const char* GetName(ENUM value)
{
auto match = nameMap.find(value);
return (match == nameMap.end() ? nullptr : match->second);
}
public:
EnumTraits() = delete;
using vector_type = std::vector<ENUM>;
using name_map_type = std::unordered_map<ENUM, const char*>;
using value_map_type = std::unordered_map<std::string, ENUM>;
private:
static const vector_type values;
static const name_map_type nameMap;
static const value_map_type valueMap;
};
struct EnumInitGuard{ constexpr const EnumInitGuard& operator=(int) const { return *this; } };
template <class T> constexpr T& operator<<=(T&& x, const EnumInitGuard&) { return x; }
...and
// EnumTraits.inl
#define ENUM_INCLUDE_MULTI
#include ENUM_INCLUDE
#undef ETRAITS
#undef EDECL
using EnumType = ENUM_DEFINE;
using TraitsType = EnumTraits<EnumType>;
using VectorType = typename TraitsType::vector_type;
using NameMapType = typename TraitsType::name_map_type;
using ValueMapType = typename TraitsType::value_map_type;
using NamePairType = typename NameMapType::value_type;
using ValuePairType = typename ValueMapType::value_type;
#define ETRAITS ; const VectorType TraitsType::values
#define EDECL(x) EnumType::x <<= EnumInitGuard()
#include ENUM_INCLUDE
#undef ETRAITS
#undef EDECL
#define ETRAITS ; const NameMapType TraitsType::nameMap
#define EDECL(x) NamePairType(EnumType::x, #x) <<= EnumInitGuard()
#include ENUM_INCLUDE
#undef ETRAITS
#undef EDECL
#define ETRAITS ; const ValueMapType TraitsType::valueMap
#define EDECL(x) ValuePairType(#x, EnumType::x) <<= EnumInitGuard()
#include ENUM_INCLUDE
#undef ETRAITS
#undef EDECL
This implementation exploits the fact that the braced list of elements of an enum definition can also be used as a braced initializer list for class member initialization.
When ETRAITS
is evaluated in the context of EnumTraits.inl
,
it expands out to a static member definition for the EnumTraits<>
class.
The EDECL
macro transforms each enum member into initializer list values which subsequently get passed into the member constructor in order to populate the enum info.
The EnumInitGuard
class is designed to consume the enum initializer values and then collapse - leaving a pure list of enum data.
c++
-like syntaxenum
and enum class
(*almost)enum
types with any numeric underlying typeenum
types with automatic, explicit, and fragmented initializer values*
In contrast to enums
, initializers in enum class
types that reference other values from the same enum must have those values fully qualified
.h/.cpp
pair for each queryable enum
macro
and include
magicclass
or namespace
scoped enums is nontrivialIntellisense will complain a bit about private member access when opening up EnumTraits.inl
, but since the expanded macros are actually defining class members, that isn't actually a problem.
The #ifndef ENUM_INCLUDE_MULTI
block at the top of the header file is a minor annoyance that could probably be shrunken down into a macro or something, but it's small enough to live with at its current size.
Declaring a namespace scoped enum requires that the enum first be forward declared inside its namespace scope, then defined in the global namespace. Additionally, any enum initializers using values of the same enum must have those values fully qualified.
namespace ns { enum MyEnum : int; }
enum ns::MyEnum : int ETRAITS
{
EDECL(AAA) = -8,
EDECL(BBB) = '8',
EDECL(CCC) = ns::MyEnum::AAA + ns::MyEnum::BBB
}