Fancy C++ Enumerations

C++ enumerations are somewhat lacking in functionality when compared to other more recent programming languages. In particular, while we can determine the integral value of an enumeration constant, it is not possible to determine the name of an enumeration constant without declaring a supporting map or array to hold the names. This post explores how to automate the creation of a name map and supporting template functions to process enumeration names, thus adding functionality to basic C++ enumerations.

Two capabilities that we would like to add to enumerations is the ability to retrieve a string representation of the enumeration name by passing an enumeration constant or variable. Additionally, to support reading enumeration constant names from a file, we would like to a function that returns an enumeration constant given a string representation of its name. Also stream insertion (operator<<) and extraction (operator>>) operators for the enumeration would be nice. Using this functionality might look like...

enum class Colors {RED, GREEN, BLUE, LAST};

cout << "First color is " << Colors::RED << endl;
cout << "What is your favorite color? ";
Colors favorite;
cin >> favorite;
cout << "Your favorite color was " << favorite << endl;

which would produce output something like...

First color is RED
What is your favorite color? GREEN
Your favorite color was GREEN

One of the biggest difficulties in implementing a solution is that it requires repeating the enumeration constant initializer list twice, once to define the C++ enum and a second time to populate a map with the value/name mappings. This of course violates the DRY Principle and becomes a maintenance nightmare. Using a macro we can define the the enumeration constants once and have the macro expand to create the C++ enumeration and the map.

#define DEFINE_ENUM(NAME, ...) \
    DEFINE_ENUM_WITH_TYPE(NAME, int, __VA_ARGS__)

#define DEFINE_ENUM_WITH_TYPE(ENUM_NAME, ENUM_TYPE, ...) \
enum class ENUM_NAME : ENUM_TYPE {__VA_ARGS__}; \
\
namespace EnumUtils { \
static EnumSupport<ENUM_NAME, ENUM_TYPE, StringLiteral(#__VA_ARGS__)> ENUM_NAME; \
} \
inline ENUM_TYPE to_value(ENUM_NAME e) noexcept { \
    return EnumUtils::ENUM_NAME.getValue(e); \
} \
inline std::string to_string(ENUM_NAME e) noexcept { \
    return EnumUtils::ENUM_NAME.getName(e); \
} \
inline std::ostream &operator<<(std::ostream &os, ENUM_NAME e) noexcept { \
    return os << to_string(e); \
} \
inline std::istream& operator>> (std::istream& is, ENUM_NAME& e) noexcept {   \
    std::string s; \
    is >> s; \
    e = EnumUtils::ENUM_NAME.getFromName(s); \
    return is;\
}

Here we define two enumerations DEFINE_ENUM() and DEFINE_ENUM_WITH_TYPE(). DEFINE_ENUM() is used if the default int base type for the enumeration is acceptable. DEFINE_ENUM_WITH_TYPE() is used if you want to specify the base type for the enumeration.

The first thing that the macro defines is the C++ enumeration. This is defined as an enum class to have better namespace encapsulation for the enumeration constants that the older enum type did not have. The variable arguments passed to the macro are going to be the enumeration's constant initializer list, so it is just passed directly to the enum class definition.

The second definition in the macro defines a support structure needed to hold the value/name mapping for the enumeration. It is static because we only need one instance of it in a compilation unit. The EnumSupport structure helps us follow the DRY Principle as its template argument is the same variable argument to the macro used by the enumeration definition that defines the enumeration constants. By prefacing the variable macro argument (__VA_ARGS__) with the pound sign ('#') use the preprocessor "stringify" operation that converts the passed argument into a C++ literal text string instead of substituting it as code. This way the string version of the enumeration initialization list can be passed to the function that initializes the enumeration value/name map which parses it and adds the value/name pairs to the map. The biggest problem we have is that EnumSupport needs to be a template and C++ does not support string literals as template parameters like it does integers. See blog post Using String Literals as C++ Template Arguments.

The next four definitions are inline global functions that operate on our enumeration. The to_value() and to_string() functions return the integral value (of the correct type) and the name (as a string) of the enumeration value passed, respectively. The stream insertion (operator<<) and stream extraction (operator>>) operators make it easy to read and write enumeration constants to a stream using the constant's name.

DEFINE_ENUM(RGBColors, RED, GREEN, BLUE, LAST);
DEFINE_ENUM_WITH_TYPE(RainbowColors, unsigned int, red=1, orange, yellow, green, blue, indigo, violet);

cout << "First RGBColor is " << RGBColors::RED << endl;
cout << "First RGBColor value is " << to_value(RGBColors::RED) << endl;
cout << "What is your favorite RGBColor? ";
RGBColors favorite;
cin >> favorite;
cout << "Your favorite RGBColor was " << favorite << endl;
cout << "Your favorite RGBColor value was " << to_value(favorite) << endl;
cout << "RGBColor with value 2 is " << EnumUtils_RGBColors.getFromValue(2) << endl;

cout << "First RainbowColor is " << to_string(RainbowColors::red) << endl;
cout << "First RainbowColor value is " << to_value(RainbowColors::red) << endl;
cout << "What is your favorite RainbowColor? ";
string favorite2;
cin >> favorite2;
RainbowColors favoriteEnum2 = EnumUtils_RainbowColors.getFromName(favorite2);
cout << "Your favorite RainbowColor was " << favoriteEnum2 << endl;
cout << "Your favorite RainbowColor value was " << EnumUtils_RainbowColors.getValue(favoriteEnum2) << endl;
cout << "RainbowColor with value 2 is " << EnumUtils_RainbowColors.getFromValue(2) << endl;

The initializer list provided to DEFINE_ENUM() and DEFINE_ENUM_WITH_TYPE() can take values assigned to the constants just as any enum initializer list can. This allows for different values than the default 0, 1, 2, etc. The support functions must be accessed using the EnumSupport object created in the EnumUtils namespace for the desired class. This object has the same name as the enumeration type name. While the global functions reduce the need for accessing the EnumSupport object, the getFromName() and getFromValue() methods can only be accessed using the EnumSupport object. Output of the example code above would look like...

First RGBColor is RED
First RGBColor value is 0
What is your favorite RGBColor? GREEN
Your favorite RGBColor was GREEN
Your favorite RGBColor value was 1
RGBColor with value 2 is BLUE
First RainbowColor is red
First RainbowColor value is 1
What is your favorite RainbowColor? violet
Your favorite RainbowColor was violet
Your favorite RainbowColor value was 7
RainbowColor with value 2 is orange

The EnumSupport structure is defined using a template. It takes template parameters for the enumeration name, enumeration type and a string representing the initializer list for the enumeration. Notice that instead of defining a static namespace variable to hold the value/name map, it instead declares a static structure method (getNameMap()) that declares a static function variable and returns it to the caller. This way we can be sure that this important map is created when we need it as the order of initialization of static variables in different compilation units is undefined in C++. The helper function setupEnumNameMap() is used by getNameMap() to initialize the map just once after it is created.

namespace EnumUtils {
    template<class T>
    void setupEnumNameMap(std::map<T, std::string> &nameMap, char const *values) {
        std::vector<std::string> enumList = StringUtils::split(values, std::regex("\\s*,\\s*"));
        std::regex re("\\s*([_\\w]+)\\s*(=\\s*(-?\\d+))?");
        std::cmatch cm;
        T enumValue = 0;
        for (std::string s: enumList) {
            if (regex_match(s.c_str(), cm, re)) {
                if (cm.size() == 4 && cm[3].length() > 0) {
                    enumValue = (T) stol(cm[3].str());
                }
            }
            nameMap[enumValue] = cm[1].str();
            ++enumValue;
        }
    }

    template<class ENUM, class TYPE, TemplateUtils::StringLiteral VALUES>
    struct EnumSupport {
        static std::map<TYPE, std::string> getNameMap() noexcept {
            static std::map<TYPE, std::string> nameMap = {};
            if (nameMap.size() == 0)
                EnumUtils::setupEnumNameMap(nameMap, VALUES.value);
            return nameMap;
        }

        inline static TYPE getValue(ENUM e) noexcept {
            return static_cast<TYPE>(e);
        }

        inline static std::string getName(ENUM e) noexcept {
            std::map<TYPE, std::string> nameMap = getNameMap();
            return nameMap[getValue(e)];
        }

        inline static ENUM getFromValue(TYPE v) {
            std::map<TYPE, std::string> nameMap = getNameMap();
            if (nameMap.contains(v)) {
                return static_cast<ENUM>(v);
            } else {
                std::stringstream ss;
                ss << "Can't convert " << v << " to enum " << typeid(ENUM).name();
                throw std::domain_error(ss.str());
            }
        }

        inline static ENUM getFromName(std::string str) {
            std::map<TYPE, std::string> nameMap = getNameMap();
            for (const auto&[key, value]: nameMap) {
                if (value == str) {
                    return static_cast<ENUM>(key);
                }
            }
            std::stringstream ss;
            ss << "Can't convert " << str << " to enum " << typeid(ENUM).name();
            throw std::domain_error(ss.str());
        }

        inline static ENUM getFromName(char const *str) {
            return getFromName(std::string(str));
        }
    };
}

Note that the EnumSupport template class makes use of a template parameter of type StringLiteral. This is a wrapper class that allows us to use a C-String literal as a template parameter (which is not normally allowed without wrapping it in StringLiteral). See Using String Literals as C++ Template Parameters

Full source for EnumUtils is as follows:

EnumUtils.h
//
// Created by Richard Lesh on 12/13/21.
//

#ifndef ENUM_UTILS_H
#define ENUM_UTILS_H

#include <exception>
#include <iostream>
#include <map>
#include <regex>
#include <sstream>
#include <stdlib.h>
#include <string>
#include "StringUtils.h"
#include "TemplateUtils.h"

namespace EnumUtils {
    template<class T>
    void setupEnumNameMap(std::map<T, std::string> &nameMap, char const *values) {
        std::vector<std::string> enumList = StringUtils::split(values, std::regex("\\s*,\\s*"));
        std::regex re("\\s*([_\\w]+)\\s*(=\\s*(-?\\d+))?");
        std::cmatch cm;
        T enumValue = 0;
        for (std::string s: enumList) {
            if (regex_match(s.c_str(), cm, re)) {
                if (cm.size() == 4 && cm[3].length() > 0) {
                    enumValue = (T) stol(cm[3].str());
                }
            }
            nameMap[enumValue] = cm[1].str();
            ++enumValue;
        }
    }

    template<class ENUM, class TYPE, TemplateUtils::StringLiteral VALUES>
    struct EnumSupport {
        static std::map<TYPE, std::string> getNameMap() noexcept {
            static std::map<TYPE, std::string> nameMap = {};
            if (nameMap.size() == 0)
                EnumUtils::setupEnumNameMap(nameMap, VALUES.value);
            return nameMap;
        }

        inline static TYPE getValue(ENUM e) noexcept {
            return static_cast<TYPE>(e);
        }

        inline static std::string getName(ENUM e) noexcept {
            std::map<TYPE, std::string> nameMap = getNameMap();
            return nameMap[getValue(e)];
        }

        inline static ENUM getFromValue(TYPE v) {
            std::map<TYPE, std::string> nameMap = getNameMap();
            if (nameMap.contains(v)) {
                return static_cast<ENUM>(v);
            } else {
                std::stringstream ss;
                ss << "Can't convert " << v << " to enum " << typeid(ENUM).name();
                throw std::domain_error(ss.str());
            }
        }

        inline static ENUM getFromName(std::string str) {
            std::map<TYPE, std::string> nameMap = getNameMap();
            for (const auto&[key, value]: nameMap) {
                if (value == str) {
                    return static_cast<ENUM>(key);
                }
            }
            std::stringstream ss;
            ss << "Can't convert " << str << " to enum " << typeid(ENUM).name();
            throw std::domain_error(ss.str());
        }

        inline static ENUM getFromName(char const *str) {
            return getFromName(std::string(str));
        }
    };
}

#define DEFINE_ENUM(NAME, ...) \
    DEFINE_ENUM_WITH_TYPE(NAME, int, __VA_ARGS__)

#define DEFINE_ENUM_WITH_TYPE(ENUM_NAME, ENUM_TYPE, ...) \
    BASE_DEFINE_ENUM_WITH_TYPE(ENUM_NAME, ENUM_TYPE, , __VA_ARGS__)

#define DEFINE_CLASS_ENUM(ENUM_NAME, ...) \
    DEFINE_CLASS_ENUM_WITH_TYPE(ENUM_NAME, int, __VA_ARGS__)

#define DEFINE_CLASS_ENUM_WITH_TYPE(ENUM_NAME, ENUM_TYPE, ...) \
    BASE_DEFINE_ENUM_WITH_TYPE(ENUM_NAME, ENUM_TYPE, friend, __VA_ARGS__)

#define BASE_DEFINE_ENUM_WITH_TYPE(ENUM_NAME, ENUM_TYPE, IS_FRIEND, ...) \
enum class ENUM_NAME : ENUM_TYPE {__VA_ARGS__}; \
\
static EnumUtils::EnumSupport<ENUM_NAME, ENUM_TYPE, TemplateUtils::StringLiteral(#__VA_ARGS__)> EnumUtils_ ## ENUM_NAME; \
IS_FRIEND inline ENUM_TYPE to_value(ENUM_NAME e) noexcept { \
    return EnumUtils_ ## ENUM_NAME.getValue(e); \
} \
IS_FRIEND inline std::string to_string(ENUM_NAME e) noexcept { \
    return EnumUtils_ ## ENUM_NAME.getName(e); \
} \
IS_FRIEND inline std::ostream &operator<<(std::ostream &os, ENUM_NAME e) noexcept { \
    return os << to_string(e); \
} \
IS_FRIEND inline std::istream& operator>> (std::istream& is, ENUM_NAME& e) noexcept {   \
    std::string s; \
    is >> s; \
    e = EnumUtils_ ## ENUM_NAME.getFromName(s); \
    return is;\
}

#endif