Introduction
As everybody knows, enums in C++ are, at best, not very useful. A lot of solutions have been proposed around the web, all of them with their advantages.
The solution proposed in this article, mainly focuses on the type safety aspect of the enumeration. It enforces type checking for each enum, avoiding undesired type conversation.
This enum class makes it hard to use it wrongly. Enumeration from one type can’t be mixed with any other data type.
How it works
Instead of directly exposing a numeric value, this enum model exposes global class instances. Internally, those classes contain the enum numerical value, but it can’t be accessed from the outside. From here, you have a bunch of class instances, each representing a unique enum value.
Implementation:
The design was the easiest part. The implementation was much harder. How do we implement this kind of
model by using a common and shareable code base? The class heritage was not very helpful here. I tried the template, but it didn't work properly. The only viable solution I ended with was the macro. With a set of generic macros, I was able to generate full classes in a standard way.
The enum macro must be spread between the header file and the source file.
Serialization
Mainly because of the serialization process, we must be able to construct an enum object from a corresponding numerical enum value. And we should also be able to get the numeric ID from an existing enum. To meet those requirements, the enum model provides the FromInt( )
and the ToInt( )
functions.
Extension
This enum model also allows extensible functionality. In the current sample, the ToString( )
function has been added. The bad side of it is, it has to be manually implemented. But it is not mandatory to use the enum. The ToString( )
function can be removed without impact.
The Problem
Switch Case… Bad switch case… The
switch
statement is a real issue for this enum model. Because in C++ only literal values must be supplied inside a
switch case
statement, I was forced to expose the numerical ID value outside the enum (other than for the serialization). If it wasn't for the
switch
, I would be able to keep it entirely private. So, to be able to benefit from switch case optimization the numeric enum ID is also public. But, I strongly suggest to not use the ID other than for
switch case
.
Restrictions
It needs a C++ compiler. It runs under MS-VC and
the GCC compiler. In its current form, dynamic libraries are not supported. But this should be fixable.
Using the code
The real macro definition to include in the header file:
#define DEFINE_ENUM_BEGIN(classname) \
class classname \
{ \
public: \
typedef int EValue; \
static classname FromInt(EValue v) {return classname(v);} \
classname(const classname& e) : m_value(e.m_value) { }; \
inline const classname& operator = (const classname& e) { m_value = e.m_value; return *this; }; \
inline bool operator == (const classname& e) const { return (m_value == e.m_value); }; \
inline bool operator != (const classname& e) const { return (m_value != e.m_value); }; \
const char* ToString() const; \
inline int ToInt() const {return (int)m_value;} \
inline int operator ()() const {return (int)m_value;}
#define DEFINE_ENUM_TYPE(classname, type, value) const static int type ## _ID = value; const static classname type;
#define DEFINE_ENUM_END(classname) \
private: \
explicit classname(const EValue v) : m_value(v) { }; \
EValue m_value; \
};
#define DEFINE_ENUM_IMPL(classname, type) const classname classname::type((classname::EValue)classname::type ## _ID);
Sample enumeration type declaration and implementation:
DEFINE_ENUM_BEGIN(CDataType)
DEFINE_ENUM_TYPE(CDataType, Unknown, 0)
DEFINE_ENUM_TYPE(CDataType, Bool, 1)
DEFINE_ENUM_TYPE(CDataType, Int, 2)
DEFINE_ENUM_TYPE(CDataType, Float, 3)
DEFINE_ENUM_END(CDataType)
DEFINE_ENUM_IMPL(CDataType, Unknown);
DEFINE_ENUM_IMPL(CDataType, Bool);
DEFINE_ENUM_IMPL(CDataType, Int);
DEFINE_ENUM_IMPL(CDataType, Float);
const char* CDataType::ToString() const
{
switch(m_value)
{
case CDataType::Bool_ID : return "Bool";
case CDataType::Int_ID : return "Int";
case CDataType::Float_ID : return "Float";
default: ASSERT(false); return "Unknown";
}
}
Usage example:
void SetDataType(CDataType ty)
{
m_DataType = ty;
}
SetDataType(CDataType::Int);
CDataType ty(CDataType::Float);
switch(ty())
{
case CDataType::Int_ID : break;
case CDataType::Float_ID : break;
default:
}
References
Somes interesting links that someone provided (sorry, your name was lost when they moved the article):