Search code examples
c++templatesc++17rtti

How is this C++ code snippet able to turn an arbitrary type into a unique integer?


Question

The EnTT library API allows you to arbitrarily assign and retrieve "pools" of different types using some metaprogramming.

How does the below code produce a unique integer for different base types. It's disregarding constantness and references which I'm also finding difficult to understand.

Runnable example

I have extracted the logic from the EnTT. You will need a C++17 compiler:

#include <iostream>

#ifndef ENTT_ID_TYPE
#include <cstdint>
#define ENTT_ID_TYPE std::uint32_t
#endif // ENTT_ID_TYPE

#ifndef ENTT_NO_ATOMIC
#include <atomic>
#define ENTT_MAYBE_ATOMIC(Type) std::atomic<Type>
#else // ENTT_NO_ATOMIC
#define ENTT_MAYBE_ATOMIC(Type) Type
#endif // ENTT_NO_ATOMIC

/*! @brief Traits class used mainly to push things across boundaries. */
template <typename> struct named_type_traits;

/**
 * @brief Specialization used to get rid of constness.
 * @tparam Type Named type.
 */
template <typename Type>
struct named_type_traits<const Type> : named_type_traits<Type> {};

/**
 * @brief Provides the member constant `value` to true if a given type has a
 * name. In all other cases, `value` is false.
 * @tparam Type Potentially named type.
 */
template <typename Type, typename = std::void_t<>>
struct is_named_type : std::false_type {};

/**
 * @brief Helper variable template.
 * @tparam Type Potentially named type.
 */
template <class Type>
constexpr auto is_named_type_v = is_named_type<Type>::value;

/**
 * @brief Helper variable template.
 * @tparam Type Potentially named type.
 */
template <class Type>
constexpr auto named_type_traits_v = named_type_traits<Type>::value;

template <typename Type, typename Family> static uint32_t runtime_type() {
  if constexpr (is_named_type_v<Type>) {
    return named_type_traits_v<Type>;
  } else {
    return Family::template type<std::decay_t<Type>>;
  }
}

/**
 * @brief Dynamic identifier generator.
 *
 * Utility class template that can be used to assign unique identifiers to types
 * at runtime. Use different specializations to create separate sets of
 * identifiers.
 */
template <typename...> class family {
  inline static ENTT_MAYBE_ATOMIC(ENTT_ID_TYPE) identifier{};

public:
  /*! @brief Unsigned integer type. */
  using family_type = ENTT_ID_TYPE;

  /*! @brief Statically generated unique identifier for the given type. */
  template <typename... Type>
  // at the time I'm writing, clang crashes during compilation if auto is used
  // instead of family_type
  inline static const family_type type = identifier++;
};

using component_family = family<struct internal_registry_component_family>;

/**
 * @brief Defines an enum class to use for opaque identifiers and a dedicate
 * `to_integer` function to convert the identifiers to their underlying type.
 * @param clazz The name to use for the enum class.
 * @param type The underlying type for the enum class.
 */
#define ENTT_OPAQUE_TYPE(clazz, type)                                          \
  enum class clazz : type {};                                                  \
  constexpr auto to_integer(const clazz id) {                                  \
    return std::underlying_type_t<clazz>(id);                                  \
  }                                                                            \
  static_assert(true)

/*! @brief Alias declaration for the most common use case. */
ENTT_OPAQUE_TYPE(component, ENTT_ID_TYPE);

template <typename T> static component type() {
  return component{runtime_type<T, component_family>()};
}

template <typename T> decltype(auto) type_to_integer() {
  return to_integer(type<T>());
}

struct ExampleStruct {};

int main() {
  std::cout << "Type int: " << type_to_integer<int>() << "." << std::endl;
  std::cout << "Type const int: " << type_to_integer<const int>() << "." << std::endl;
  std::cout << "Type double: " << type_to_integer<double>() << "." << std::endl;
  std::cout << "Type float: " << type_to_integer<float>() << "." << std::endl;
  std::cout << "Type ExampleStruct: " << type_to_integer<ExampleStruct>() << "." << std::endl;
  std::cout << "Type &ExampleStruct: " << type_to_integer<ExampleStruct&>() << "." << std::endl;
}

Example output

Type int: 0.
Type const int: 0.
Type double: 1.
Type float: 2.
Type ExampleStruct: 3.
Type &ExampleStruct: 3.

Solution

  • The shown code is full of supporting glue for portability and other syntactic sugar that slightly obscures its underlying implementation. It's easier to understand the core concept of what's going on here by considering a much more simplified example:

    #include <iostream>
    
    class family {
        inline static int identifier=0;
    
    public:
    
        template <typename... Type>
        inline static const int type = identifier++;
    };
    
    int main()
    {
        std::cout << "int: " << family::type<int> << std::endl;
        std::cout << "const char *: "
              << family::type<const char *> << std::endl;
    
        std::cout << "int again: " << family::type<int> << std::endl;
    
        return 0;
    }
    

    g++ 9.2.1, with -std=c++17 produces the following output:

    int: 0
    const char *: 1
    int again: 0
    

    family gets initialized with the identifier member default-initialized to 0.

    The underlying C++ core concept here is that a template gets instantiated the first time its referenced. The first time type<int> is referenced it gets instantiated, and gets default-initialized from the expression identifier++, which initializes this type instance, and increments the identifier. Each new type instance gets initialized the same, incrementing the identifier again. using a previously used type simply uses the already-instantiated template with its originally-initialized value.

    That's the basic concept being used here. The rest of the shown code is several kinds of window dressing, i.e. using std::atomic if available, and picking the best type for the counter.

    Note that this trick is full of minefields when multiple translation units are involved. The above approach only works without any unexpected surprises when it's used only in one translation unit. Those templates do seem to have some provisions for using multiple translation units, but with an independent counter for each translation unit. That's another complication that obscures the shown code...