Contents

Bidirectional mapping between enum values and types

Recently, while working with a glue code integrating low level C APIs in C++ I stumbled upon a problem where I needed to map enum values to types (and vice-versa).

Problem definition

Imagine you’ve got a factory function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum class FactoryTypes {
    TypeA,
    TypeB,
    TypeC,
};

struct NonCovariantTypeA {};
struct NonCovariantTypeB {};
struct NonCovariantTypeC {};

// TODO what to return here?
// auto create(FactoryTypes t);

Okay, so the create function must be a template but how to determine the return type? Well, mapping from enum Types to any of the types is needed. It’s easy to create one thanks to C++’s constant value template specialisation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
template <FactoryTypes> struct EnumToType;

template <>
struct EnumToType<FactoryTypes::TypeA> {
    using type = NonCovariantTypeA;
};

template <>
struct EnumToType<FactoryTypes::TypeB> {
    using type = NonCovariantTypeB;
};

template <>
struct EnumToType<FactoryTypes::TypeC> {
    using type = NonCovariantTypeC;
};

With the above mapping, the implementation of create function is quite trivial.

1
2
3
4
template <Types TypesV>
typename EnumToType<TypesV>::type create() {
    ...
}

Cool, job done. But, is it possible to create reverse mapping? Mapping from any of the aforementioned types back to its respective enum? Well, that can be done in an equally trivial manner by introduction of another specialised type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
template <typename T>
struct FactoryTypeToEnum;

template <>
struct FactoryTypeToEnum<NonCovariantTypeA> {
    static constexpr FactoryTypes value = FactoryTypes::TypeA;
};

template <>
struct FactoryTypeToEnum<NonCovariantTypeB> {
    static constexpr FactoryTypes value = FactoryTypes::TypeB;
};

template <>
struct FactoryTypeToEnum<NonCovariantTypeC> {
    static constexpr FactoryTypes value = FactoryTypes::TypeC;
};

The above can be implemented using consteval function as well but, for the time being, I’m gonna stick with a more traditional syntax.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template <typename T>
constexpr auto FactoryTypeToEnum() {
    if constexpr (std::is_same_v<T, NonCovariantTypeA>) {
        return FactoryTypes::TypeA;
    } else if constexpr (std::is_same_v<T, NonCovariantTypeB>) {
        return FactoryTypes::TypeB; 
    } else if constexpr (std::is_same_v<T, NonCovariantTypeC>) {
        return FactoryTypes::TypeC;
    }
}

This will definitely work but it doesn’t feel optimal as there are two mappings that have to be independently maintained. It would be perfect to have only one mapping capable of bidirectional resolution between types and enumerations.

Limitations

To implement two-way mapping between types and enums, I’ll have rely on some assumptions and inevitably will need to introduce some limitations.

Adjustment to the enum type

I will need two extra values in the enum for it to be supported by the discussed code. First and Last so, the resulting enum will be:

1
2
3
4
5
6
7
8
9
enum class FactoryTypes {
    First,

    TypeA,
    TypeB,
    TypeC,

    Last,
};

These are commonly introduced within enums so this might not be a problem but can be a deal breaker in some case so, it’s worth to keep that in mind.

Enums must be contiguous

Second assumption is that all values within an enum must form a contiguous range. Enums containing non-contiquous ranges, with explicitly defined values won’t be supported e.g.:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// WARNING: non-contiquous enums are not supported
enum class FactoryTypes {
    First,

    TypeA = 1000,
    TypeB = 2000,
    TypeC = 3000,

    Last,
};

The reason for this will become obvious later on.

Enums must map to distinct types

This limitation is fairly obvious as well. It’s not really a problem to map a set of enumerations to exactly the same type but the implication is that it won’t be possible to do the reverse opposite. Therefore, enum values must map to distinct types for the reverse mapping to be possible.

Mapping enums to types

Let’s start with a declaration of the mapping type:

1
template <typename ET, ET EV> struct EnumToType;

Let’s first take care of the simplest case - which is mapping enums to types.

The following specialisation should take care of that nicely.

1
2
3
4
5
#define DECL_MAPPING(EV, T)                                                    \
  template <> struct EnumToType<decltype(EV), EV> {                            \
    using enum_type = decltype(EV);                                            \
    using type = T;                                                            \
  }

With the following, it’s possible to create mappings the following way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DECL_MAPPING(FactoryTypes::TypeA, NonCovariantTypeA);
DECL_MAPPING(FactoryTypes::TypeB, NonCovariantTypeB);
DECL_MAPPING(FactoryTypes::TypeC, NonCovariantTypeC);

// The mapping can be used like so
typename EnumToType<FactoryTypes, FactoryTypes::TypeA>::type instantation{};

// ... and a quick test
static_assert(
    std::is_same_v<typename EnumToType<FactoryTypes, FactoryTypes::TypeA>::type,
                   NonCovariantTypeA>,
    "");

This can be simplified slightly with a using statement similarly as commonly done in STL:

1
2
3
4
5
6
template <typename ET, ET EV>
using EnumToTypeT = typename EnumToType<ET, EV>::type;

static_assert(
    std::is_same_v<EnumToTypeT<FactoryTypes, FactoryTypes::TypeA>, NonCovariantTypeA>,
    "");

That’s nice, half of the problem is solved. How to implement the reverse mapping though?

Mapping types to enums

In essence, finding an enum value for a given type is the same as performing an index operation on a type list. Which means that the problem boils down to declaring a typelist containing all types. I’ve discussed typelists thoroughly in my previous post. It’s required to have familiarity with concepts discussed there in order to proceed. From now on, I’ll assume you did that and we are basically on the same page (quite literally :)).

Initial type list

Let’s first specialise the EnumToType mapping for the first enum value.

1
2
3
4
#define DECL_MAPPING_BEGIN(ET)                                                 \
  template <> struct EnumToType<ET, ET::First> {                               \
    using tmap = typelist::TypeList<void>;                                     \
  }

This declares a type list with a single type void in it. With that in place, the specialisation for other enum values has to be altered to contain tmap as well. But what should it be? Each new tmap should effectively append a new type to already existing type list (the one declared for prior enum value). So, the first prerequisite is to be able to determine prior enum value, given the one at hand. For that purpose I’m gonna define this simple type:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template <typename ET, ET EV> struct PriorEnum {
  static constexpr ET value =
      EV == ET::First
          ? ET::First
          : static_cast<ET>(static_cast<std::underlying_type_t<ET>>(EV) - 1);
};

template <typename ET, ET EV>
inline constexpr ET PriorEnumV = PriorEnum<ET, EV>::value;

// can be used like so
static_assert(PriorEnumV<FactoryTypes, FactoryTypes::TypeB> == FactoryTypes::TypeA, "");

This could be simplified with std::to_underlying but I want to stick with c++17 compatibility. It’s a simple function which just decrements the given enum value and casts it back to the given enum type. Additionally it clamps the value to First so you can never go below the smallest enumeration available.

I already have the initial declaration of the type list for First enumeration, now all subsequent ones should just append new type to this map. Here’s the updated EnumToType:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template <typename ET, ET EV>
using EnumToTypeM = typename EnumToType<ET, EV>::tmap;

#define DECL_MAPPING(EV, T)                                                    \
  template <> struct EnumToType<decltype(EV), EV> {                            \
    using enum_type = decltype(EV);                                            \
    using type = T;                                                            \
    static constexpr enum_type value = EV;                                     \
    static constexpr enum_type prior_value = PriorEnumV<enum_type, value>;     \
    using prior_tmap = EnumToTypeM<enum_type, prior_value>;                    \
    using tmap = typelist::AppendT<prior_tmap, type>;                          \
  }

Having the above, the last thing required is the mapping for Last which is a generalised case:

1
#define DECL_MAPPING_END(ET) DECL_MAPPING(ET::Last, void)

The complete type list can be access through Last enumeration value so, to index a type we have to do the following:

1
2
#define INDEX_TYPE(ET, t)                                                      \
  static_cast<ET>(typelist::IndexV<EnumToTypeM<ET, ET::Last>, t>)

For the sake of completeness, I’m gonna declare a helper macro for extracting a type associated with an enum:

1
2
#define ENUM_TO_TYPE(EV) \
    EnumToTypeT<decltype(EV), EV>

That concludes the implementation.

Usage example

Having the following enums and types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
enum class FactoryTypes {
    First,

    TypeA,
    TypeB,
    TypeC,

    Last,
};

struct NonCovariantTypeA {};
struct NonCovariantTypeB {};
struct NonCovariantTypeC {};

A bidirectional mapping can be declared the following way:

1
2
3
4
5
DECL_MAPPING_BEGIN(FactoryTypes);
DECL_MAPPING(FactoryTypes::TypeA, NonCovariantTypeA);
DECL_MAPPING(FactoryTypes::TypeB, NonCovariantTypeB);
DECL_MAPPING(FactoryTypes::TypeC, NonCovariantTypeC);
DECL_MAPPING_END(FactoryTypes);

To extract a type associated with an enum, we can use ENUM_TO_TYPE so, the definition of the factory function used in my first example might look like so:

1
2
3
4
template <Types TypesV>
ENUM_TO_TYPE(TypesV) create() {
    ...
}

Enum lookup by type is possible as well:

1
2
3
void foo() {
    const auto enumValue = INDEX_TYPE(FactoryTypes, NonCovariantTypeB);
}

Conclusion

The code discussed here in its entirety is available in my gitlab repo.