Skip to content

Instantly share code, notes, and snippets.

@kudaba
Last active December 17, 2020 22:52
Show Gist options
  • Save kudaba/dc2d720813cc9416dd93bd4806c699e7 to your computer and use it in GitHub Desktop.
Save kudaba/dc2d720813cc9416dd93bd4806c699e7 to your computer and use it in GitHub Desktop.
Better c++ enum
#include "GC_Enum.h"
//-------------------------------------------------------------------------------------------------
// Build enum metadata from generated enum string
//-------------------------------------------------------------------------------------------------
GC_EnumMetaData::GC_EnumMetaData(char const* aString, StringPart* someParts, uint aCount, int aStart, int aDefault)
: myEnumString(aString)
, myParts(someParts)
, myCount(aCount)
, myStartValue(aStart)
, myDefault(aDefault)
{
// parse value labels from string
uint pos = 0;
for (uint i = 0; i < aCount; ++i)
{
GC_ASSERT(aString[pos]);
someParts[i].myStart = (uint16)pos;
// search until comma, whitespace or end of string for the name of the enum
while (GC_IsWord(aString[pos])) ++pos;
GC_ASSERT(pos != someParts[i].myStart);
someParts[i].myEnd = (uint16)pos;
// skip until next word
while (aString[pos] && !GC_IsWord(aString[pos])) ++pos;
}
GC_ASSERT(pos < USHRT_MAX); // a 65k long enum... that's a bit excessive
}
//-------------------------------------------------------------------------------------------------
// Lookup enum name from metadata
//-------------------------------------------------------------------------------------------------
GC_EnumString GC_EnumMetaData::ToString(int aValue) const
{
aValue -= myStartValue;
if (aValue < 0 ||
aValue >= (int)myCount)
{
return GC_EnumString();
}
StringPart const& part = myParts[aValue];
return GC_EnumString(myEnumString + part.myStart, part.Length());
}
//-------------------------------------------------------------------------------------------------
// see if name is present in metadata
//-------------------------------------------------------------------------------------------------
bool GC_EnumMetaData::FromString(char const* aString, int& aFoundValue) const
{
uint len = GC_Strlen(aString);
for (uint i = 0; i < myCount; ++i)
{
if (len == myParts[i].Length())
{
if (GC_Memeq(aString, myEnumString + myParts[i].myStart, len))
{
aFoundValue = myStartValue + i;
return true;
}
}
}
if (GC_Memeq(aString, "Default", sizeof("Default")))
{
aFoundValue = myDefault;
return true;
}
return false;
}
#pragma once
//-------------------------------------------------------------------------------------------------
// GC_Enum is a way to declare a typesafe enum that can be serialized to and from a string without
// any dynamic allocation overhead. The one big restriction with this is that it only works with sequential
// values, however they can start at any value.
//
// Reserved words: Count, Start, End, Default
//
// Declare enum using one of the macros: GC_DECLARE_ENUM(Name, Value1, Value2, Value3)
//
// Then use it like you would any strongly types enum:
// Name anEnum = Name::Value1;
//
// Now you get fun features such age
// anEnum.ToString()
// anEnum = Name::FromString("Value1");
// Name::Start and Name::End
//-------------------------------------------------------------------------------------------------
typedef GC_StaticString<32> GC_EnumString;
//-------------------------------------------------------------------------------------------------
// Used to Store information about the enum, mainly for serialization to/from strings
//-------------------------------------------------------------------------------------------------
class GC_EnumMetaData
{
public:
// Hold the start and end of a part of an enum string
struct StringPart
{
uint16 myStart;
uint16 myEnd;
uint16 Length() const { return myEnd - myStart; }
};
// Construct with a comma separated string and pre-allocated set of string parts to hold the string offsets.
GC_EnumMetaData(char const* aString, StringPart* someParts, uint aCount, int aStart, int aDefault);
GC_EnumString ToString(int aValue) const;
bool FromString(char const* aString, int& aFoundValue) const;
private:
char const* myEnumString;
StringPart const* myParts;
uint myCount;
int myStartValue;
int myDefault;
};
//-------------------------------------------------------------------------------------------------
// GC_Enum functions as the actual type through a typedef
//-------------------------------------------------------------------------------------------------
template<typename EnumBase>
struct GC_Enum : public EnumBase
{
typedef typename EnumBase::Type Type;
//-------------------------------------------------------------------------------------------------
// for loop helper to iterate all values. While I'm not a fan of the generated code, it's not that commonly used.
// Usage: for (Enum i : Enum::Range())
//-------------------------------------------------------------------------------------------------
struct Iter
{
GC_FORCEINLINE void operator++() { ++myValue; }
GC_FORCEINLINE bool operator!=(Type anEnd) const { return myValue != anEnd; }
GC_FORCEINLINE Type operator*() const { return (Type)myValue; }
int myValue;
};
struct Range
{
GC_FORCEINLINE Iter begin() { return { EnumBase::Start }; }
GC_FORCEINLINE Type end() { return EnumBase::End; }
};
constexpr GC_Enum() : myValue(EnumBase::Default) {}
constexpr GC_Enum(Type aValue) : myValue(aValue) {}
constexpr explicit GC_Enum(int aValue) : myValue((Type)aValue) {}
constexpr GC_Enum(GC_Enum const& aEnum) : myValue(aEnum.myValue) {}
constexpr operator Type() const { return myValue; }
constexpr Type GetEnd() const { return Type::End; }
constexpr Type operator++() { myValue = Type(myValue + 1); return myValue; }
constexpr Type operator++(int) { Type tmp = myValue; myValue = Type(myValue + 1); return tmp; }
constexpr Type operator--() { myValue = Type(myValue - 1); return myValue; }
constexpr Type operator--(int) { Type tmp = myValue; myValue = Type(myValue - 1); return tmp; }
GC_EnumString ToString() const
{
return ourMetaData.ToString(myValue);
}
static GC_EnumString ToString(Type aValue)
{
return ourMetaData.ToString(aValue);
}
static GC_EnumString ToString(int aValue)
{
return ourMetaData.ToString(aValue);
}
bool SetString(char const* aString)
{
int value = myValue;
bool found = ourMetaData.FromString(aString, value);
myValue = (Type)value;
return found;
}
static bool FromString(char const* aString, Type& aValueOut)
{
int value;
if (!ourMetaData.FromString(aString, value))
return false;
aValueOut = Type(value);
}
static bool FromString(char const* aString, int& value)
{
return ourMetaData.FromString(aString, value);
}
static Type FromString(char const* aString)
{
int value = EnumBase::Start;
ourMetaData.FromString(aString, value);
return Type(value);
}
Type myValue;
static GC_EnumMetaData const ourMetaData;
};
template<typename EnumBase>
GC_EnumMetaData const GC_Enum<EnumBase>::ourMetaData = EnumBase::GetMetaData();
//------------------------------------------------------------------------------------------------------------
// Declaration variant to change the first value
//------------------------------------------------------------------------------------------------------------
#define GC_DECLARE_ENUM_WITH_START_AND_DEFAULT(type, start, defaultValue, first, ...) \
struct type##Base \
{ \
enum Type : int8 { first = start, Start = start, __VA_ARGS__, End, Default = defaultValue }; \
enum { Count = End - Start }; \
protected: \
static inline GC_EnumMetaData GetMetaData() \
{ \
static GC_EnumMetaData::StringPart ourParts[Count]; \
return GC_EnumMetaData(#first", "#__VA_ARGS__, ourParts, Count, Start, Default); \
} \
};\
typedef GC_Enum<type##Base> type;
//------------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------
#define GC_DECLARE_ENUM_WITH_DEFAULT(type, defaultValue, first, ...) GC_DECLARE_ENUM_WITH_START_AND_DEFAULT(type, 0, defaultValue, first, __VA_ARGS__)
#define GC_DECLARE_ENUM_WITH_START(type, start, first, ...) GC_DECLARE_ENUM_WITH_START_AND_DEFAULT(type, start, first, first, __VA_ARGS__)
#define GC_DECLARE_ENUM(type, first, ...) GC_DECLARE_ENUM_WITH_START_AND_DEFAULT(type, 0, first, first, __VA_ARGS__)
//------------------------------------------------------------------------------------------------------------
// allow some use of enum type without including it
//------------------------------------------------------------------------------------------------------------
#define GC_FORWARD_DECLARE_ENUM(type) struct type##Info; typedef GC_Enum<type##Info> type
//------------------------------------------------------------------------------------------------------------
// Add handling for normally hidden value when using switch statements.
// Can optionally be used with default: to catch all unused values, but prevert to use on it's own
// switch (en) {
// GC_ENUM_UNREACHABLE(en);
// }
// OR
// switch (en) {
// default: GC_ENUM_UNREACHABLE(en);
// }
//
// Doing it this way allows us to keep the error about unused elements if you don't add the default label.
//------------------------------------------------------------------------------------------------------------
#define GC_ENUM_UNREACHABLE(value) case decltype(value)::End: GC_ASSERT(false, "%s not handled", value.ToString()); break;

This enum class is meant to solve a lot annoyances working with enums in c++ in a simple, optimal way.

Usage

GC_DECLARE_ENUM(GC_MouseKey,
	Left,
	Middle,
	Right,
	Button4, // 4 and 5 are typically back and forward respectively
	Button5,
	Button6,
	Button7,
	Button8);

GC_MouseKey key = GC_MouseKey::Middle;
printf(key.ToString());

key = GC_MouseKey::FromString("Right");

for (GC_MouseKey key : GC_MouseKey::Range()) {}

switch (key)
{
	GC_ENUM_UNREACHABLE(key);
	case GC_MouseKey::Left:
	...
}

Features

  • Scoped
  • Auto generated Start,End,Count
  • No dynamic allocations (only premain initialization)
  • Implicit conversion to int, explicit from int
  • Helper to iterate enum values
  • Helper to allow Switch warnings about unused valued to be enabled (required because End is the last value + 1)

Limitations

  • Doesn't work with switch compile errrors unless you handle Count
  • Defaults underlying type to char (int8) which limits to 127 consecutive values. (could be fixed with some template shinnanegans)

Implementation

Unfortunately, this code references a few things that are in my private repo

  • GC_StaticString - fully featured string class that implicitly converts to char const*
  • GC_IsWord - Tests is a character is a letter,number or underscore '_'
  • GC_Strlen - wrapper for strlen
  • GC_Memeq - wrapper for memcmp() == 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment