API библиотеки EnTT позволяет произвольно назначать и извлекать «пулы» разных типов с использованием некоторого метапрограммирования.
Как приведенный ниже код создает уникальное целое число для разных базовых типов. Это игнорирование постоянства и ссылок, которые мне также трудно понять.
Я извлек логику из 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>() << "
";
}