Как этот фрагмент кода C ++ способен превратить произвольный тип в уникальное целое число?

Вопрос

API библиотеки EnTT позволяет произвольно назначать и извлекать «пулы» разных типов с использованием некоторого метапрограммирования.

Как приведенный ниже код создает уникальное целое число для разных базовых типов. Это игнорирование постоянства и ссылок, которые мне также трудно понять.

Runnable пример

Я извлек логику из EnTT . Вам понадобится компилятор C ++ 17:

#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;
}

Пример вывода

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

Всего 2 ответа


Показанный код полон поддерживающего клея для переносимости и другого синтаксического сахара, который слегка затеняет его основную реализацию. Проще понять основную концепцию происходящего здесь, рассмотрев гораздо более упрощенный пример:

#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, с -std=c++17 выдает следующий вывод:

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

family инициализируется с identifier умолчанию инициализируемым равным 0.

Основная концепция ядра C ++ заключается в том, что шаблон создается при первом обращении к нему. При первом обращении к type<int> он создается, и инициализируется по умолчанию из identifier++ выражения identifier++ , который инициализирует этот экземпляр type и увеличивает identifier . Каждый экземпляр нового type инициализируется одинаково, снова увеличивая identifier . использование ранее использованного type просто использует уже созданный экземпляр шаблона с его первоначально инициализированным значением.

Это основная концепция, используемая здесь. Остальная часть показанного кода представляет собой несколько видов оформления витрин, то есть использование std::atomic если доступно, и выбор наилучшего типа для счетчика.

Обратите внимание, что этот прием полон минных полей, когда задействованы несколько единиц перевода. Вышеуказанный подход работает без каких-либо неожиданных сюрпризов, когда он используется только в одной единице перевода. Эти шаблоны, похоже, содержат некоторые положения для использования нескольких единиц перевода, но с независимым счетчиком для каждой единицы перевода. Это еще одно осложнение, которое скрывает показанный код ...


С большим количеством дополнительного механизма для решения различных крайних случаев код по сути делает что-то вроде этого:

#include <iostream>
#include <atomic>

struct family
{
    inline static std::atomic<int> identifier{};
    template < typename T >
    inline static const int type = identifier++;
};

int main()
{
    std::cout << family::type<int> << "
";
    std::cout << family::type<int> << "
";
    std::cout << family::type<float> << "
";
}

Каждый раз, когда type<T> используется впервые, каждый T инициализируется identifier++ поэтому каждый тип получает различное число.

Дополнительный код выполняет такие вещи, как проверка того, что const int , int и const int& получают одинаковое значение (в этом простом примере это не так).

Вы можете заставить его работать для const и ссылок с помощью дополнительной функции:

template < typename T >
int type_to_integer()
{
    using nr = std::remove_reference_t<T>;
    using nc = std::remove_cv_t<nr>;
    return family::type<nc>;
}

int main()
{
    std::cout << type_to_integer<int>() << "
";
    std::cout << type_to_integer<int>() << "
";
    std::cout << type_to_integer<const int>() << "
";
    std::cout << type_to_integer<const int&>() << "
";
    std::cout << type_to_integer<float>() << "
";
}

Есть идеи?

10000