核心功能
目录
简介
EnTT 附带了一系列核心功能,主要供库的其他部分使用。
其中许多工具在日常开发中同样实用。因此,有必要对其进行说明,以免在需要时重复造轮子。
Any 即任意类型
EnTT 提供了自己的 any 类型。考虑到 C++17 已引入 std::any,这看似多余,但实际上并非如此。
首先,std::any 返回的 type 是 std::type_info 的 const 引用,这是一个实现定义的类,并非所有软件都希望在代码中看到它。此外,无法将其与库的类型系统及其集成的 RTTI 支持绑定。
any 的 API 与其著名的标准库对应物非常相似,主要是因为该类具有相同的目的:作为任意类型值的不透明容器。
实例还依赖于一种称为 小缓冲区优化 (small buffer optimization) 的知名技术和伪造的 vtable 来最小化内存分配次数。
创建 any 类型的对象(无论是否为空)非常简单:
// 空容器
entt::any empty{};
// 包含 int 的容器
entt::any any{0};
// 就地类型构造
entt::any in_place_type{std::in_place_type<int>, 42};
// 接管已存在的、动态分配的对象的拥有权
entt::any in_place{std::in_place, std::make_unique<int>(42).release()};
或者,make_any 函数可实现相同目的。它要求始终显式指定类型,且不支持接管所有权:
entt::any any = entt::make_any<int>(42);
在所有情况下,any 类都负责在需要时销毁包含的元素,而不管特定对象使用的存储策略如何。
此外,any 实例不绑定于实际类型。因此,当为其分配一个与其包含的类型不同的新对象时,包装器会重新配置。
还有一种方法可以直接为 entt::any 包含的变量赋值,而不必替换它。当对象在 别名模式 (aliasing mode) 下使用时,这特别有用,如下所述:
entt::any any{42};
entt::any value{3};
// 按拷贝赋值
any.assign(value);
// 按移动赋值
any.assign(std::move(value));
any 类会检查类型信息,并根据情况检查原始类型是否支持拷贝或移动赋值。
在所有情况下,assign 函数都会返回一个布尔值,成功时为 true,否则为 false。
如果对包含的对象类型有疑问,type 成员函数会返回与其元素关联的 type_info 的 const 引用;如果容器为空,则返回 type_id<void>()。
在比较两个 any 对象时,内部也会使用该类型:
if(any == empty) { /* ... */ }
在这种情况下,在进行比较之前,会验证两个对象的 type 是否确实相同。
有关 type_info 的工作原理及比较的潜在风险的更多详细信息,请参阅 EnTT 类型系统文档。
该类的一个特别有趣的功能是,它还可以用作 const 和非 const 引用的不透明容器:
int value = 42;
entt::any any{std::in_place_type<int &>(value)};
entt::any cany = entt::make_any<const int &>(value);
entt::any fwd = entt::forward_as_any(value);
any.emplace<const int &>(value);
换句话说,只要明确指示 any 构造 别名 (alias),它就会充当指向原始实例的指针,而不是在内部进行复制或移动。包含的对象永远不会被销毁,用户必须确保其生命周期长于容器。
同样,可以从现有对象创建 any 的非拥有拷贝 (non-owning copies):
// 别名构造函数
entt::any ref = other.as_ref();
在这种情况下,原始容器是实际持有对象,还是已经作为未托管元素的引用,都无关紧要。这样创建的新实例不会创建副本,仅作为原始项目的引用。
值得一提的是,虽然对于非 const 引用一切都能透明地工作,但对于 const 引用则存在一些例外。
特别是,在包装了 const 引用的 any 的非 const 实例上调用 data 成员函数时,在任何情况下都会返回空指针。
要将 any 实例转换为特定类型,库提供了一组 any_cast 函数,在各方面都与其著名的标准库对应物相似。
唯一的区别是,在 EnTT 中,它们不会抛出异常,而只会在 debug 模式下触发 assert,否则在 release 模式下误用会导致未定义行为。
小缓冲区优化
any 类使用一种称为 小缓冲区优化 (small buffer optimization) 的技术来尽可能减少内存分配次数。
any 实例的默认保留大小为 sizeof(double[2])。但是,如果需要,这也是可配置的。事实上,any 被定义为 basic_any<Len> 的别名,其中 Len 即为上述大小。
用户可以轻松设置自定义大小或定义自己的别名:
using my_any = entt::basic_any<sizeof(double[4])>;
此功能除了允许选择最适合应用程序需求的大小外,还提供了在构造期间强制动态创建对象的可能性。
换言之,如果大小为 0,any 将禁用小缓冲区优化,并始终动态分配对象(别名情况除外)。
对齐要求
对齐要求是可选的,默认情况下,对于大小不超过所提供大小的任何对象,采用最严格(最大)的对齐要求。
它作为可选的第二个参数提供,紧跟在内部存储的所需大小之后:
using my_any = entt::basic_any<sizeof(double[4]), alignof(double[4])>;
basic_any 类模板会在每种情况下检查对齐要求(即使未提供),并可能决定不使用小缓冲区优化以满足这些要求。
位运算
一些通用工具,例如快速取模函数:
const std::size_t result = entt::fast_mod(value, modulus);
其中 modulus 必须是 2 的幂。此类操作在性能上远优于基本取模运算,因此在许多领域更受青睐。
压缩 pair
compressed_pair 类主要为内部使用而设计,且远非功能完备,它完全兑现了其承诺:通过利用 空基类优化 (Empty Base Class Optimization, EBCO) 来尝试减小 pair 的大小。
此类 不是 std::pair 的无缝替代品 (drop-in replacement)。但是,当减少内存使用比拥有一些炫酷但可能无用的功能更重要时,它提供了足够的功能,是一个很好的替代方案。
尽管其 API 与 std::pair 非常接近(除了模板参数是从构造函数推导的,因此没有 entt::make_compressed_pair),但主要区别在于,出于实现要求,first 和 second 是函数:
entt::compressed_pair pair{0, 3.};
pair.first() = 42;
因此没有太多需要描述的。建议依赖文档和直觉。归根结底,它只是一个 pair,仅此而已。
枚举作为位掩码
有时将枚举用作位掩码 (bitmask) 很有用。然而,enum class 并不真正适合此目的。主要问题是它们不会隐式转换为其底层类型。
因此,选择在于使用老式枚举(带有我不想在此讨论的所有问题)或编写 丑陋 的代码。
幸运的是,还有第三种方法:在全局作用域中添加足够的运算符,以透明地将 enum class 视为位掩码。
最终目标是编写如下代码(或者可能是更有意义的代码,但这应该能让人领会且同时保持简单):
enum class my_flag {
unknown = 0x01,
enabled = 0x02,
disabled = 0x04
};
const my_flag flags = my_flag::enabled;
const bool is_enabled = !!(flags & my_flag::enabled);
将所有运算符添加到全局作用域的问题在于,即使不需要时它们也会生效,从而带来引入难以处理的错误的风险。
然而,C++ 提供了足够的工具来规避此问题。特别是,库要求用户注册应启用位掩码支持的 enum class:
template<>
struct entt::enum_as_bitmask<my_flag>
: std::true_type
{};
在处理由第三方库定义且用户无法控制的 enum class 时,这很方便。然而,这也很冗长,可以通过向 enum class 本身添加特定值来避免:
enum class my_flag {
unknown = 0x01,
enabled = 0x02,
disabled = 0x04,
_entt_enum_as_bitmask
};
在这种情况下,无需特化 enum_as_bitmask trait,因为 EnTT 会自动检测该标志并启用位掩码支持。
一旦注册了 enum class(通过某种方式),最常用的运算符(如 &、| 以及 &= 和 |=)就可供使用。
有关运算符的完整列表,请参阅官方文档。
哈希字符串
哈希字符串 (hashed strings) 是代码库中人类可读的标识符,它们在运行时转换为数值,因此不会影响性能。
该类具有一个隐式的 constexpr 构造函数,可解析一串字符。创建后,可以通过 data 成员函数获取原始字符串,或将实例转换为数字。
哈希字符串非常适合需要常量表达式的任何地方。如果谨慎使用,运行时不会发生 字符串到数字 的转换。
使用示例:
auto load(entt::hashed_string::hash_type resource) {
// 使用资源的数值表示来加载并返回它
}
auto resource = load(entt::hashed_string{"gui/background"});
还有一个专用于哈希字符串的 用户定义字面量 (user defined literal),使其更具 用户友好性:
using namespace entt::literals;
constexpr auto str = "text"_hs;
EnTT 中的用户定义字面量包含在 entt::literals 命名空间中。因此,在每次使用前,必须显式包含整个命名空间或选择性地包含感兴趣的字面量,这有点像 std::literals。
该类还提供了在运行时创建哈希字符串的必要功能:
std::string orig{"text"};
// 创建功能完整的哈希字符串...
entt::hashed_string str{orig.c_str()};
// ... 或仅计算唯一标识符
const auto hash = entt::hashed_string::value(orig.c_str());
不应在紧密循环 (tight loops) 中利用此可能性,因为计算发生在运行时而不是编译时。因此,它可能会在一定程度上影响性能。
宽字符
hashed_string 类是 basic_hashed_string<char> 的别名。要使用 C++ 类型进行宽字符表示,还存在 basic_hashed_string<wchar_t> 的别名 hashed_wstring。
在这种情况下,用于即时创建哈希字符串的用户定义字面量是 _hws:
constexpr auto str = L"text"_hws;
hashed_wstring 的哈希类型与其对应物相同。
冲突
哈希字符串类内部使用 FNV-1a 对字符串进行哈希处理。由于 鸽巢原理 (pigeonhole principle),冲突是可能的。这是一个事实。
在处理哈希函数时,没有解决冲突问题的银弹 (silver bullet)。在这种情况下,最好的解决方案可能是放弃。仅此而已。
毕竟,人类可读的唯一标识符并非严格定义,用户也无法完全控制。在这种情况下,选择一个略有不同的标识符可能是使冲突消失的最佳解决方案。
迭代器
编写和使用迭代器并不总是那么容易。通常它还会导致代码重复。
EnTT 试图通过提供一些旨在简化这项艰苦工作的工具来克服这个问题。
输入迭代器指针
在编写解引用时返回就地构造 (in-place constructed) 值的输入迭代器时,弄清楚 value_type 是什么以及如何使其表现得像一个成熟的指针并不总是那么直接。
相反,在迭代器本身上提供一个始终有效且没有太多复杂性的 operator-> 将非常有用。
输入迭代器指针正是为此而设计的。它是一个小型类,包装就地构造的值,并在其上添加一些函数,使其适合与输入迭代器一起使用:
struct iterator_type {
using value_type = std::pair<first_type, second_type>;
using pointer = input_iterator_pointer<value_type>;
using reference = value_type;
using difference_type = std::ptrdiff_t;
using iterator_category = std::input_iterator_tag;
// ...
}
库在内部广泛使用此类。在许多情况下,返回的迭代器的 value_type 只是一个输入迭代器指针。
Iota 迭代器
在等待 C++20 期间,此迭代器接受一个整数值并返回特定范围内的所有元素:
entt::iota_iterator first{0};
entt::iota_iterator last{100};
for(; first != last; ++first) {
int value = *first;
// ...
}
未来,views 将取代此类。同时,当需要向用户返回一系列整数值时,库会对其进行一些有趣的利用。
可迭代适配器
通常,容器类提供 begin 和 end 成员函数(及其 const 对应物)用于迭代。
但是,一个类可能会提供多种迭代方法,或允许用户迭代不同的 元素 (elements) 集合。
可迭代适配器 (iterable adaptor) 是一个工具类,在这种情况下可简化数据的使用和访问。
它接受一对迭代器(或一个迭代器和一个哨兵 (sentinel)),并提供一个具有所有预期方法(如 begin、end 等)的 可迭代 (iterable) 对象。
库广泛使用此类。
例如,考虑 views,可以对其进行迭代以访问实体,同时也提供一种方法来获取一个可迭代对象,该对象一次性返回实体和组件的 tuple。
另一个例子是 registry 类,它允许用户通过返回用于此目的的可迭代对象来迭代其存储。
内存
EnTT 中有一些工具可以以某种方式与内存交互。
其中一些旨在简化(内部或外部)分配器感知 (allocator aware) 容器的实现。另一些旨在帮助开发者解决日常问题。
前者非常具体,针对小众问题。例如,有些工具旨在帮助人们忘记 POCCA、POCMA 或 POCS 等首字母缩略词的含义。
我不会在这里详细描述它们。相反,我建议对该主题感兴趣的人阅读内联文档。
分配器感知 unique pointer
C++ 中(至少到 C++20 为止)一件棘手的事情是,shared pointer 支持分配器,而 unique pointer 却不支持。
目前有一个提案也展示了(除其他外)如何在没有任何编译器支持的情况下实现这一点。
allocate_unique 函数遵循此提案,化被动为主动:
std::unique_ptr<my_type, entt::allocation_deleter<allocator_type>> ptr = entt::allocate_unique<my_type>(allocator, arguments);
尽管内部实现与标准提案略有不同,但此函数提供的 API 是该特性的无缝替代品 (drop-in replacement)。
单态模式
单态 (monostate) 模式通常被作为基于单例 (singleton) 的配置系统的替代方案提出。
这正是它在 EnTT 中的目的。此外,此实现在设计上是线程安全的(希望如此)。
键是整数值(可通过哈希字符串轻松获取),值是 int 或 bool 等基本类型。不同类型的值可以与每个键关联,甚至可以一次关联多个。
因此,在赋值和尝试读回数据时,应注意使用相同的类型。否则,有可能会出现意外结果。
使用示例:
entt::monostate<entt::hashed_string{"mykey"}>{} = true;
entt::monostate<"mykey"_hs>{} = 42;
// ...
const bool b = entt::monostate<"mykey"_hs>{};
const int i = entt::monostate<entt::hashed_string{"mykey"}>{};
类型支持
EnTT 提供各种类型的一些基本信息。
它还提供标准库中尚未提供或永远不会提供的附加功能。
内置 RTTI 支持
运行时类型识别 (RTTI) 支持是 C++ 世界中最常被禁用的功能之一,尤其是在游戏领域。无论原因如何,在运行时无法依赖不透明的类型信息通常是一件憾事。
库试图通过提供一个内置系统来填补这一空白,该系统虽然不能作为替代品,但非常接近,并提供与其对应物类似的信息。
基本上,整个系统依赖于少数几个类。特别是:
-
与给定类型关联的唯一顺序标识符:
auto index = entt::type_index<a_type>::value();不保证返回值在不同的运行中保持稳定。
然而,它作为关联和无序关联容器中的索引,或用于 vector 或 array 中的位置访问,非常有用。如果需要,也可以使用外部生成器。事实上,
type_index可以按类型特化,或使用 concept 进行约束,以允许更精细的特化,例如:template<typename Type> requires requires { { Type::index() } -> std::same_as<entt::id_type>; } struct entt::type_index<Type> { static entt::id_type value() noexcept { return Type::index(); } };在这种情况下,索引 必须 按顺序生成。
该工具在EnTT中被广泛使用。非顺序生成索引会破坏一个假设,并可能导致不良行为。 -
与给定类型关联的哈希值:
auto hash = entt::type_hash<a_type>::value();通常,
type_hash公开的value函数也是constexpr的,但这并不能保证适用于所有编译器和平台(尽管它对最知名和最流行的编译器有效)。此函数 可以 为其自身目的使用语言的非标准特性。这使得提供在不同运行中保持稳定的编译时标识符成为可能。
用户可以通过ENTT_STANDARD_CPP宏定义阻止库使用这些特性。在这种情况下,无法保证标识符在多次执行中保持稳定。此外,它们是在运行时生成的,不再是编译时的东西。与
type_index一样,type_hash也可以特化或使用 concept 约束,以便全局或按类型/按 trait 自定义其行为。 -
与给定类型关联的名称:
auto name = entt::type_name<a_type>::value();此值提取自所用编译器通常提供的一些信息。因此,它可能因编译器而异,并且在信息不可用时可能为空。
例如,给定以下类:struct my_type { /* ... */ };使用 GCC 或 CLang 编译时名称为
my_type,使用 MSVC 时为struct my_type。
大多数时候,名称也是在编译时检索的,因此始终通过std::string_view返回。用户可以轻松访问并根据需要修改它,例如删除struct一词以标准化结果。出于显而易见的原因,EnTT不会这样做,否则它将在运行时创建一个新字符串。此函数 可以 为其自身目的使用语言的非标准特性。用户可以通过
ENTT_STANDARD_CPP宏定义阻止库使用这些特性。在这种情况下,名称只是为空。与
type_index一样,type_name也可以特化或使用 concept 约束,以便全局或按类型/按 trait 自定义其行为。
然后将这些组合成工具,旨在提供与标准库提供的 API 有些相似的 API。
类型信息
type_info 类不是 std::type_info 的无缝替代品 (drop-in replacement),但可以提供类似的信息,这些信息不是实现定义的,也不需要启用 RTTI。
因此,它们有时甚至比通过其他方式获得的信息更可靠。
其类型定义了一个不透明的类,该类也是可拷贝和可移动的。
此类型的对象通常由 type_id 函数返回:
// 按类型
auto info = entt::type_id<a_type>();
// 按值
auto other = entt::type_id(42);
这样接收到的所有元素不过是具有静态存储期的 type_info 实例的 const 引用。
这便于保存整个对象,而只需付出一个指针的代价。然而,没有什么能阻止直接构造 type_info 对象:
entt::type_info info{std::in_place_type<int>};
以下是 type_info 提供的信息:
-
与给定类型关联的索引:
auto idx = entt::type_id<a_type>().index();这也是以下代码的别名:
auto idx = entt::type_index<std::remove_cvref_t<a_type>>::value(); -
与给定类型关联的哈希值:
auto hash = entt::type_id<a_type>().hash();这也是以下代码的别名:
auto hash = entt::type_hash<std::remove_cvref_t<a_type>>::value(); -
与给定类型关联的名称:
auto name = entt::type_id<my_type>().name();这也是以下代码的别名:
auto name = entt::type_name<std::remove_cvref_t<a_type>>::value();
如果所有访问的特性在编译时都可用,则 type_info 类也是完全 constexpr 的。然而,这无法提前保证,主要取决于所使用的编译器以及上述类的任何特化。
近乎唯一的标识符
由于 type_hash 的默认非标准编译时实现利用了哈希字符串,因此可能会发生两个类型被分配相同哈希值的情况。
事实上,虽然这种情况非常罕见,但并未完全排除。
另一种两个类型被分配相同标识符的情况是,来自不同上下文(例如在运行时加载的两个或多个库)的类具有相同的全限定名。在这种情况下,type_name 为这两个类型返回相同的值。
幸运的是,有几种简单的方法可以处理这个问题:
-
最简单的方法是定义
ENTT_STANDARD_CPP宏。事实上,运行时标识符不会遇到同样的问题。然而,此解决方案在库未链接的插件系统中效果不佳。 -
另一种可能性是为冲突的类型之一特化
type_name类,以便为其分配自定义标识符。这可能是最简单的解决方案,同时也保留了该工具的特性。 -
完全自定义的标识符生成策略(例如基于 enum class 或预处理步骤)可能代表另一种选择。
这些只是解决该问题的可能方法的一些示例,但还有许多其他方法。如上所述,由于用户对其类型拥有完全控制权,因此无论如何这个问题都很容易解决,不必过于担心。
无论如何,极大概率不会遇到冲突。
类型萃取
标准模板库中不存在但在日常生活中可能有用的一些工具和类型萃取 (type traits)。
此列表 并非 详尽无遗,仅包含一些最有用的类。有关此模块提供功能的更多信息,请参阅内联文档。
Size of
如果用户向标准运算符 sizeof 提供函数或不完整类型,它会报错。另一方面,即使应用于空类类型,也保证结果始终为非零。
这个小型类结合了两者,并提供了一种在所有情况下都有效的 sizeof 替代方案,如果类型不受支持则返回零:
const auto size = entt::size_of_v<void>;
Is applicable
标准库以多种形式提供了出色的 std::is_invocable trait。它接受一个函数类型和一系列参数,如果满足条件则返回 true。
此外,还为用户提供了 std::apply,这是一个用于组合可调用元素和参数 tuple 的工具。
因此,拥有一个也接受 tuple-like 类型参数的 std::is_invocable 变体以完善功能是个好主意:
constexpr bool result = entt::is_applicable<Func, std::tuple<a_type, another_type>>;
此 trait 构建在 std::is_invocable 之上,除了展开 tuple-like 类型并简化调用点代码外,别无他用。
Constness as
一个轻松将类型的 const 属性 (constness) 转移到另一种类型的工具:
// 由于 src_type 的 const 属性,type 为 const dst_type
using type = entt::constness_as_t<dst_type, const src_type>;
该 trait 受语言规则的约束。例如,在引用之间 转移 const 属性不会产生预期的效果。
成员类类型
C++17 引入的 auto 模板参数使得简化许多类模板和模板函数成为可能,但当成员作为模板参数传递时,也使得类类型变得不透明。
此工具的目的是在几行代码中提取类类型:
template<typename Member>
using clazz = entt::member_class_t<Member>;
第 N 个参数
一个快速查找函数、成员函数或数据成员的第 n 个参数的工具(用于对不透明类型的盲操作):
using type = entt::nth_argument_t<1u, decltype(&clazz::member)>;
如果需要,重载函数的消歧由用户负责。
整型常量
由于 std::integral_constant 的形式要求同时指定类型和该类型的值,可能有些烦人,因此存在一个更用户友好的快捷方式来创建整型常量 (integral constants)。
此快捷方式是别名模板 entt::integral_constant:
constexpr auto constant = entt::integral_constant<42>;
在其他用途中,当与哈希字符串结合使用时,它有助于将标签 (tags) 定义为人类可读的 名称,否则将需要实际的类型:
constexpr auto enemy_tag = entt::integral_constant<"enemy"_hs>;
registry.emplace<enemy_tag>(entity);
标签
id_type 类型在 EnTT 中非常重要且被广泛使用。因此,存在一个更用户友好的快捷方式来基于它创建常量。
此快捷方式是别名模板 entt::tag。
如果与哈希字符串结合使用,它有助于在需要类型的地方使用人类可读的名称。例如:
registry.emplace<entt::tag<"enemy"_hs>>(entity);
然而,这不是唯一允许的用法。实际上,任何可转换为 id_type 的值都是很好的候选者,例如无作用域枚举 (unscoped enum) 的命名常量。
类型列表与值列表
没有任何受人尊敬的库会缺少急需的 类型列表 (type list)。
EnTT 也不例外,除了提供专用于非类型模板参数的 value_list 对应物外,还提供了(并在内部广泛使用)type_list 类型。
以下是类型列表附带功能的(可能不完整的)列表:
type_list_element[_t]获取类型列表的第 N 个元素。type_list_index[_v]获取类型列表给定元素的索引。type_list_cat[_t]和便捷的operator+用于连接类型列表。type_list_unique[_t]从类型列表中移除重复类型。type_list_contains[_v]了解类型列表是否包含给定类型。type_list_diff[_t]从类型列表中移除类型。type_list_transform[_t]转换 (transform) 范围并创建另一个类型列表。
我也非常确定,随着需求变得明显,随着时间的推移会添加越来越多的工具。
许多这些功能也存在于专用于值列表 (value lists) 的版本中。因此我们有 value_list_element[_v] 以及 value_list_cat[_t] 等等。
唯一顺序标识符
有时,能够在编译时或运行时为类型赋予唯一的、顺序的数字标识符是有用的。
针对此问题有许多不同的解决方案,我本可以使用其中之一。然而,我决定花时间定义几个完全拥抱现代 C++ 所提供特性的工具。
编译时生成器
为了在编译时生成顺序数字标识符,EnTT 提供了 ident 类模板:
// 为给定类型定义标识符
using id = entt::ident<a_type, another_type>;
// ...
switch(a_type_identifier) {
case id::value<a_type>:
// ...
break;
case id::value<another_type>:
// ...
break;
default:
// ...
}
这就是此类模板提供的全部:一个包含给定类型的数字标识符的 value 内联变量。它可以在任何需要常量表达式的上下文中使用。
只要列表保持不变,标识符也保证在不同的运行中保持稳定。如果在需要移除某个类型的生产环境中使用,占位符 (placeholder) 可以帮助保持其他标识符不变:
template<typename>
struct ignore_type {};
using id = entt::ident<
a_type_still_valid,
ignore_type<no_longer_valid_type>,
another_type_still_valid
>;
在代码库中看起来可能有点丑陋,但至少能完成任务。
运行时生成器
family 类模板有助于在运行时为类型生成顺序数字标识符:
// 定义自定义生成器
using id = entt::family<struct my_tag>;
// ...
const auto a_type_id = id::value<a_type>;
const auto another_type_id = id::value<another_type>;
这就是 family 提供的全部:一个包含给定类型的数字标识符的 value 内联变量。
生成器是可定制的,以便在需要时为不同目的获取不同的 序列 (sequences)。
不保证标识符在不同的运行中保持稳定。事实上,它主要取决于执行流程。
实用工具
无法抗拒向库中添加某种工具的诱惑。事实上,EnTT 还提供了一些工具来简化开发者的生活:
-
entt::overload:一个用于根据其函数类型消除不同重载歧义的工具。它适用于自由函数和成员函数。
考虑以下定义:struct clazz { void bar(int) {} void bar() {} };此工具可用于获取 正确 的重载,如下所示:
auto *member = entt::overload<void(int)>(&clazz::bar);上面这行代码在字面上等价于:
auto *member = static_cast<void(clazz:: *)(int)>(&clazz::bar);只是更易读且输入更短。
-
entt::overloaded:一个小型类模板,用于从一堆 lambda 或 functor 创建一个具有重载operator()的新类型。
例如:entt::overloaded func{ [](int value) { /* ... */ }, [](char value) { /* ... */ } }; func(42); func('c');在进行元编程并必须向函数传递同时支持多种类型的可调用对象时相当有用。
-
entt::y_combinator:这是 Y 组合子 (y-combinator) 的 C++ 实现。如果不清楚它是什么,可能就不需要这个工具。
下面是一个展示其用法的小示例:entt::y_combinator gauss([](const auto &self, auto value) -> unsigned int { return value ? (value + self(value-1u)) : 0; }); const auto result = gauss(3u);乍一看可能有些复杂,但确实有效。不幸的是,该语言无法使其做得更好。
这是 EnTT 提供的(实际上很少的)工具的简要概述。随着时间的推移,这个列表可能会增长,但每个工具的规模将保持相当小,正如迄今为止的情况一样。