Fill in the runtime data at compile time with templates


I have a specific situation in which I would like to prepare some runtime structures at compile time without the need to duplicate code.

I have two structs that I use to register at compile time some types for a compiler I wrote:

using TypeID = u8;

template<typename T, typename TYPE_ID, TYPE_ID I>
struct TypeHelper
  static constexpr TYPE_ID value = std::integral_constant<TYPE_ID, I>::value;

template<typename T> struct Type : TypeHelper<T, u8, __COUNTER__> { static_assert(!std::is_same<T,T>::value, "Must specialize for type!"); };

These are used in a config header with a macro that specialize Type<T> for multiple types I require:

using type_size = unsigned char;

#define GET_NTH_MACRO(_1,_2,_3, NAME,...) NAME


constexpr TypeID TYPE_##_NAME_ = __COUNTER__; \
template<> struct Type<_TYPE_> : TypeHelper<_TYPE_, type_size, TYPE_##_NAME_> { \
  static constexpr const char* name = #_NAME_; \


so that these expand to

constexpr TypeID TYPE_void = 2;
template<> struct Type<void> : TypeHelper<void, type_size, TYPE_void> { static constexpr const char* name = "void"; };

constexpr TypeID TYPE_s64 = 3;
template<> struct Type<s64> : TypeHelper<s64, type_size, TYPE_s64> { static constexpr const char* name = "s64"; };

constexpr TypeID TYPE_s32 = 4;
template<> struct Type<s32> : TypeHelper<s32, type_size, TYPE_s32> { static constexpr const char* name = "s32"; };

This is working fine but the compiler also requires some runtime information about these types, so in addition to this I must define some auxiliary functions like

static TypeID typeForIdent(const std::string& name);
static const char* nameForType(TypeID type);
static void mapTypeName(TypeID type, const std::string& name);

inline bool isSigned(TypeID type)
  return type == Type<s8>::value || type == Type<s16>::value ||
  type == Type<s32>::value || type == Type<s64>::value;

and similar functions.

These functions must work without template arguments, so that TypeID must be a normal argument. But I'm required to initialize such data in a separate part of code, eg:

mapTypeName(Type<s32>::value, "s32");

which uses a static std::unordered_map<TypeID, std::string>. Of course this also implies that I must maintain twice the code when most of the information is already available at compile time through the types defines.

I was wondering if there's some obscure trick which I'm missing which could coalesce these so that REGISTER_TYPE macro also registers the runtime information. I haven't come with anything yet but maybe there's a clever way to manage this.

If you are not particularly concerned with the performance of registering your run-time data into the map, you could simply use an inline function that returns a reference to a static map instance, and generate "dummy" registrar instances in your REGISTER_TYPE macro that fill the map in their constructor.

inline auto& registration_map()
    static std::unordered_map<int, std::string> m;
    return m;

struct registrar
    registrar(int id, std::string s)
        registration_map()[id] = std::move(s);

template <typename T>
struct TypeHelper { };

#define CAT3_IMPL(a, b, c) a ## b ## c
#define CAT3(a, b, c) CAT3_IMPL(a, b, c)

#define REGISTER_TYPE(id, type) \
    template<> struct TypeHelper<type> { }; \
    [[maybe_unused]] registrar CAT3(unused_registrar_, __LINE__, type) {id, #type};

REGISTER_TYPE(2, double)

int main()
    assert(registration_map()[0] == "int");
    assert(registration_map()[1] == "float");
    assert(registration_map()[2] == "double");

Full example on wandbox.


  • You may get repeated registrations if the same REGISTER_TYPE is included in multiple translation units.

  • CAT3(unused_registrar_, __LINE__, type) is used to generate an unique name that will not collide with other REGISTER_TYPE expansions.