Implementing Python-Style Enums in C++

It has been proven over and over again that we suffer from a severe form of NIH. Today, we tried to reinvent the wheel once again, but with rockets attached, and tackled the beauty of C++17 by creating a usable enum implementation.

The current openage enums sometimes produce linker errors on macOS and warnings on other systems:

(`instanciation of enum required here, but no definition is available`)
...
[ 35%] Building CXX object libopenage/CMakeFiles/libopenage.dir/log/stdout_logsink.cpp.o
In file included from /home/jj/devel/openage/libopenage/log/stdout_logsink.cpp:3:
In file included from /home/jj/devel/openage/libopenage/log/stdout_logsink.h:5:
In file included from /home/jj/devel/openage/libopenage/log/logsink.h:8:
In file included from /home/jj/devel/openage/libopenage/log/level.h:8:
/home/jj/devel/openage/libopenage/log/../util/enum.h:99:17: warning: instantiation of variable 'openage::util::Enum::data' required here, but no definition is available [-Wundefined-var-template]
                return &this->data[this->id].second;
                              ^
/home/jj/devel/openage/libopenage/log/stdout_logsink.cpp:16:33: note: in instantiation of member function 'openage::util::Enum::operator->' requested here
        std::cout << "\x1b[" << msg.lvl->colorcode << "m" << std::setw(4) << msg.lvl->name << "\x1b[m" " ";
                                       ^
/home/jj/devel/openage/libopenage/log/../util/enum.h:129:19: note: forward declaration of template entity is here
        static data_type data;
                         ^
/home/jj/devel/openage/libopenage/log/../util/enum.h:99:17: note: add an explicit instantiation declaration to suppress this warning if 'openage::util::Enum::data' is explicitly instantiated in another translation unit
                return &this->data[this->id].second;
                              ^
1 warning generated.
...
 

We tried getting rid of that warning several times, but that always led into a deep rabbit hole of linker errors. Now we were fed up and decided to get rid of our old enum implementation.

Why?

But why this own enum implementation?

  • Usage exactly like the enum class
  • Have a string representation of each enum value
  • Allow member methods for the enum type
  • Everything at compile time and accross TUs without funny extern definitions

After some experiments, we implemented it with a wrapper class (EnumValueContainer) which has implicit conversions to the EnumValue type. The actual enum values are constexpr static members of a subclass of EnumValue. We need this in order to use LogLevel for both containing the all possible enum values, being the type for the enum like an enum class name, and using it as non-const type that references to one of the possible values.

This way, the container can store a reference to its static member, i.e. a handle to a enum value like you know from enum class!

The remaining problem was member methods, especially because the "container" class type should also be usable a enum-value type. To achieve this, we had the idea of using a mixin class LogLevelMethods with CRTP. This is what we came up with:

enum.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#pragma once

#include <iostream>
#include <typeinfo>
#include <type_traits>
#include <cxxabi.h>

template<typename DerivedType, typename NumericType=int>
struct EnumValue {
    // enum values cannot be copied
    EnumValue(const EnumValue &other) = delete;
    EnumValue &operator =(const EnumValue &other) = delete;

    // enum values are equal if the pointers are equal.
    constexpr bool operator ==(const DerivedType &other) const {
        return (this == &other);
    }

    /* SNIP */

    friend std::ostream &operator <<(std::ostream &os, const DerivedType &arg) {
        int status;
        os << abi::__cxa_demangle(typeid(DerivedType).name(), 0, 0, &status) << "::" << arg.name;
        return os;
    }

    const char *name;
    NumericType numeric;
};

template<typename DerivedType>
struct EnumValueContainer {
    using this_type = EnumValueContainer<DerivedType>;

    const DerivedType &value;

    constexpr EnumValueContainer(const DerivedType &value) : value{value} {}

    // implicit conversion operator!
    constexpr operator const DerivedType &() const {
        return this->value;
    }

    constexpr EnumValueContainer &operator =(const DerivedType &value) {
        this->value = value;
    }

    constexpr bool operator ==(const this_type &other) const {
        return (this->value == other.value);
    }

    /* SNIP */

    friend std::ostream &operator <<(std::ostream &os, const this_type &arg) {
        os << arg.value;
        return os;
    }
};


struct NOVAL {};
struct VAL {};


template <typename T, typename novalue, typename ET=void>
struct EnumMethods {
    template <typename X=novalue>
    typename std::enable_if<std::is_same<X, NOVAL>::value, const T*>::type
    get_this() const {
        return static_cast<const T*>(this);
    };

    template <typename X=novalue>
    typename std::enable_if<std::is_same<X, VAL>::value, const T*>::type
    get_this() const {
        return static_cast<const T*>(&(static_cast<const ET*>(this))->value);
    };
};

loglevel.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#pragma once

#include "enum.h"


template <typename T, typename novalue, typename ET=void>
struct LogLevelMethods : EnumMethods<T, novalue, ET> {
    void foo() const {
        std::cout << "foo is " << this->get_this()->name << std::endl;
    }
};


/** Here, new member values for each enum value can be added */
struct LogLevelValue : EnumValue<LogLevelValue>, LogLevelMethods<LogLevelValue, NOVAL> {
    const char *color_code;
};


/** Usage of the first design attempt for the new enum */
struct LogLevel : EnumValueContainer<LogLevelValue>, LogLevelMethods<LogLevelValue, VAL, LogLevel> {
    using EnumValueContainer<LogLevelValue>::EnumValueContainer;

    static constexpr LogLevelValue debug = {{"debug", 10}, {}, "31;1"};
    static constexpr LogLevelValue info = {{"info", 20}, {}, "32"};
};

Thus, it is now possible to do this:

1
2
3
LogLevel a = LogLevel::info;  // store a handle to the static constexpr member!
a.foo();                      // call the enum "member" method!
LogLevel::info.foo();         // same, but without the LogLevel wrapper!

But why does that work?

The = assignment uses the implicit conversion from LogLevelValue to LogLevel. The latter stores a reference to LogLevelValue.

The a.foo() call is even more obscure: The magic with LogLevelMethods effectively "redirects" the operator . from LogLevel to LogLevelValue through LogLevelMethods via EnumMethods.

EnumMethods adds the foo method to both the LogLevel container and each LogLevelValue directly, and can convert the this pointer accordingly to reach the per-enum-value data. If we kept this, we probably would have added macros to simplify the template shenanigans in the declarations of LogLevelValue and LogLevel.

Containing almost standard-library-levels of template magic, we didn't exactly want to commit this to the repo.

Improvements

The much easier variant, which works without a redirect/"overload" of the operator . is to just use operator -> instead, which can actually be overloaded without hacks:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct EnumValueContainer {
    /* ... */
    constexpr const DerivedType *operator ->() const {
        return &(this->value);
    }
    /* ... */
}

struct LogLevelValue : EnumValue<LogLevelValue> {
    const char *color_code;
    // of course, more members and functions could be added here

    void foo() const {
        std::cout << "foo: " << this->name << std::endl;
    }
}

/** Usage of the "final" design for our new enum */
struct LogLevel : EnumValueContainer<LogLevelValue> {
    using EnumValueContainer<LogLevelValue>::EnumValueContainer;

    static constexpr LogLevelValue debug = {{"debug", 10}, "31;1"};
    static constexpr LogLevelValue info = {{"info", 20}, "32"};
};

Now, we can just call it with ->:

1
2
3
4
5
int main() {
    LogLevel lvl = LogLevel::debug;
    lvl->foo();
    return 0;
}

Mind that all this only works with C++17.

Here is the full code for our new enum (GPLv3 or later): #### Enum definition:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
// Copyright 2018 the openage authors, GPLv3 or later.
#pragma once

#include <iostream>
#include <typeinfo>
#include <cxxabi.h>


template<typename DerivedType, typename NumericType=int>
struct EnumValue {
    // enum values cannot be copied
    EnumValue(const EnumValue &other) = delete;
    EnumValue &operator =(const EnumValue &other) = delete;

    // enum values are equal if the pointers are equal.
    constexpr bool operator ==(const DerivedType &other) const {
        return (this == &other);
    }

    constexpr bool operator !=(const DerivedType &other) const {
        return !(*this == other);
    }

    constexpr bool operator <=(const DerivedType &other) const {
        return this->numeric <= other.numeric;
    }

    constexpr bool operator <(const DerivedType &other) const {
        return this->numeric < other.numeric;
    }

    constexpr bool operator >=(const DerivedType &other) const {
        return this->numeric >= other.numeric;
    }

    constexpr bool operator >(const DerivedType &other) const {
        return this->numeric > other.numeric;
    }

    friend std::ostream &operator <<(std::ostream &os, const DerivedType &arg) {
        int status;
        os << abi::__cxa_demangle(typeid(DerivedType).name(), 0, 0, &status) << "::" << arg.name;
        return os;
    }

    const char *name;
    NumericType numeric;
};


template<typename DerivedType>
struct EnumValueContainer {
    using this_type = EnumValueContainer<DerivedType>;

    const DerivedType &value;

    constexpr EnumValueContainer(const DerivedType &value) : value{value} {}

    constexpr operator const DerivedType &() const {
        return this->value;
    }

    constexpr EnumValueContainer &operator =(const DerivedType &value) {
        this->value = value;
        return *this;
    }

    constexpr const DerivedType *operator ->() const {
        return &(this->value);
    }

    constexpr bool operator ==(const this_type &other) const {
        return (this->value == other.value);
    }

    constexpr bool operator !=(const this_type &other) const {
        return (this->value != other.value);
    }

    constexpr bool operator <=(const this_type &other) const {
        return this->value <= other.value;
    }

    constexpr bool operator <(const this_type &other) const {
        return this->value < other.value;
    }

    constexpr bool operator >=(const this_type &other) const {
        return this->value >= other.value;
    }

    constexpr bool operator >(const this_type &other) const {
        return this->value > other.value;
    }

    friend std::ostream &operator <<(std::ostream &os, const this_type &arg) {
        os << arg.value;
        return os;
    }
};
#### Usage:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#pragma once

#include "enum.h"


struct LogLevelValue : EnumValue<LogLevelValue> {
    const char *color_code;

    void bar() const {
        std::cout << "bar is " << this->name << " and " << this->color_code << std::endl;
    }
};


struct LogLevel : EnumValueContainer<LogLevelValue> {
    using EnumValueContainer<LogLevelValue>::EnumValueContainer;

    static constexpr LogLevelValue debug = {{"debug", 10}, "31;1"};
    static constexpr LogLevelValue info = {{"info", 20}, "32"};
};

int main() {
    LogLevel l = LogLevel::debug;
    std::cout << l << " => " << l->bar() << std::endl;
    std::cout << (LogLevel::debug < LogLevel::info) << std::endl;

    return 0;
}

Oh C++, such an adventure game.

links

stalking