这个 C++ 代码片段如何能够将任意类型转换为唯一整数?
How is this C++ code snippet able to turn an arbitrary type into a unique integer?
问题
EnTT 库 API 允许您使用一些元编程任意分配和检索 "pools" 不同类型。
下面的代码如何为不同的基本类型生成唯一的整数。它无视我也很难理解的恒定性和参考。
可运行示例
我已经从 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.
有很多额外的机制来处理各种边缘情况,代码基本上是在做这样的事情:
#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> << "\n";
std::cout << family::type<int> << "\n";
std::cout << family::type<float> << "\n";
}
每次 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>() << "\n";
std::cout << type_to_integer<int>() << "\n";
std::cout << type_to_integer<const int>() << "\n";
std::cout << type_to_integer<const int&>() << "\n";
std::cout << type_to_integer<float>() << "\n";
}
显示的代码充满了可移植性和其他稍微模糊其底层实现的语法糖的支持胶水。通过考虑一个更简化的例子,可以更容易地理解这里发生的事情的核心概念:
#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++
进行默认初始化,这会初始化此 type
实例,并递增 identifier
。每个新的 type
实例都以相同的方式初始化,再次递增 identifier
。 using a previously used type
简单地使用已经实例化的模板及其最初初始化的值。
这是这里使用的基本概念。显示代码的其余部分是几种 window 敷料,即如果可用,使用 std::atomic
,并为计数器选择最佳类型。
请注意,当涉及多个翻译单元时,此技巧充满了雷区。上述方法仅在仅在一个翻译单元中使用时才有效。这些模板似乎确实有一些使用多个翻译单元的规定,但每个翻译单元都有一个独立的计数器。这是另一个使显示的代码变得模糊的并发症...
问题
EnTT 库 API 允许您使用一些元编程任意分配和检索 "pools" 不同类型。
下面的代码如何为不同的基本类型生成唯一的整数。它无视我也很难理解的恒定性和参考。
可运行示例
我已经从 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.
有很多额外的机制来处理各种边缘情况,代码基本上是在做这样的事情:
#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> << "\n";
std::cout << family::type<int> << "\n";
std::cout << family::type<float> << "\n";
}
每次 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>() << "\n";
std::cout << type_to_integer<int>() << "\n";
std::cout << type_to_integer<const int>() << "\n";
std::cout << type_to_integer<const int&>() << "\n";
std::cout << type_to_integer<float>() << "\n";
}
显示的代码充满了可移植性和其他稍微模糊其底层实现的语法糖的支持胶水。通过考虑一个更简化的例子,可以更容易地理解这里发生的事情的核心概念:
#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++
进行默认初始化,这会初始化此 type
实例,并递增 identifier
。每个新的 type
实例都以相同的方式初始化,再次递增 identifier
。 using a previously used type
简单地使用已经实例化的模板及其最初初始化的值。
这是这里使用的基本概念。显示代码的其余部分是几种 window 敷料,即如果可用,使用 std::atomic
,并为计数器选择最佳类型。
请注意,当涉及多个翻译单元时,此技巧充满了雷区。上述方法仅在仅在一个翻译单元中使用时才有效。这些模板似乎确实有一些使用多个翻译单元的规定,但每个翻译单元都有一个独立的计数器。这是另一个使显示的代码变得模糊的并发症...