EnTT: 核心功能 Core functionalities

整理 EnTT 核心工具,包括 any、位操作、压缩对、哈希字符串、迭代器、monostate、类型支持和实用工具。

EnTT 带有大量功能, 这些功能大多由库的其他部分使用, 其中许多工具在日常工作中也很有用, 因此, 值得对它们进行描述, 以免在需要时重新发明轮子

entt::any

entt::any 是 EnTT 提供的一个类似于 C++17 中 std::any 的类型, 但它针对特定场景进行了优化, 尤其是与 EnTT 的类型系统深度集成

关键特性

  1. 与 EnTT 类型系统的集成
    不同于 std::any 使用的 std::type_info (依赖具体实现的类型信息), entt::any 与 EnTT 的自定义类型系统集成, 更加适合与 EnTT 的 运行时类型信息 RTTI, 即 entt::meta 一起使用

  2. 小对象优化 SBO, Small Buffer Optimization
    使用 SBO 技术直接在分配的内存中存储小对象, 减少了堆分配操作, 显著提高性能

  3. 伪虚表 Fake VTable
    通过 伪虚表 实现类型擦除功能, 不依赖传统的虚函数调用, 降低运行时开销

  4. 灵活的对象管理
    支持多种对象构造方式

    // 空容器
    entt::any empty{};
 
    // 直接初始化
    entt::any any{42};
 
    // 原地构造
    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);

无论使用何种存储策略, entt::any 都能确保正确销毁内部对象

  1. 类型动态重配置
    entt::any 不绑定到特定类型, 当容器被赋予不同类型的新对象时, 它会自动销毁原对象并重新配置自身

  2. 别名模式 Aliasing Mode
    可以存储指向已有对象的引用 (包括 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 不会拷贝或管理对象的生命周期, 用户需确保原始对象的生命周期比容器长

  1. 非拥有副本
    可以通过 as_ref() 从现有对象创建一个非拥有的引用副本
    entt::any ref = other.as_ref();

在这种情况下, 原始容器是否实际保存了对象或已作为非托管元素的引用并不重要, 这样创建的新实例不会创建副本, 而仅作为原始项目的引用

  1. 高效的类型检查
    提供 type 成员函数返回所包含对象的类型信息, 而不依赖 std::type_info, 在比较两个 entt::any 对象时, 会首先检查类型是否一致
    if (any == empty) {
    // 类型相同且内容相等
    }
  1. 安全类型转换
    提供类似 std::any_cast 的类型转换函数, 但在使用错误时不会抛出异常, 而是在调试模式下触发 assert, 发布模式下表现为 未定义行为

std::any 的对比

特性entt::anystd::any
类型系统集成 EnTT 自定义类型系统使用 std::type_info
小对象优化(SBO)实现依赖具体编译器
引用支持支持 const 和非 const 引用不支持
异常安全错误时触发 assert抛出异常
与 EnTT 的集成深度集成,适配 EnTT 的类型系统通用解决方案
伪虚表

小对象优化 Small Buffer Optimization, SBO

entt::any 使用了一种称为 小对象优化 的技术, 尽可能减少动态分配的次数, 从而提高性能

默认配置

默认情况下, entt::any 内部保留了一块大小为 sizeof(double[2]) 的固定缓冲区, 这意味着, 如果存储的对象大小小于或等于 sizeof(double[2]), 该对象会直接存储在缓冲区中, 而不会进行堆分配, 如果对象的大小超过了这个限制, entt::any 会动态分配内存来存储对象

自定义缓冲区大小

用户可以通过配置缓冲区大小来更好地适配应用的需求, 通过模板参数设置大小: entt::anyentt::basic_any<Len> 的别名, 其中 Len 是缓冲区的大小, 默认值为 sizeof(double[2]), 用户可以通过定义自己的别名来调整缓冲区大小

// 定义一个缓冲区大小为 sizeof(double[4]) 的 any 类型
using my_any = entt::basic_any<sizeof(double[4])>;

强制动态分配

如果将大小设置为 0, 则会禁用小对象优化, 所有对象都会通过动态分配创建 (别名模式除外), 这种模式适用于需要完全动态存储的场景, 或者当你明确知道大部分对象都超出默认缓冲区时可以避免额外的开销

对齐要求 Alignment requirement

entt::any 支持用户自定义对象对齐要求,这是它设计的一个重要特性,尤其在需要处理高对齐要求的对象时,显得尤为重要

默认对齐

默认情况下, entt::any 使用最严格的对齐要求, 它会选择内部存储中所有可能存储对象所需的最大对齐方式, 对于小对象优化, 它会确保内部缓冲区符合对象的对齐需求, 如果对齐要求超过缓冲区的能力, entt::any 会选择动态分配内存以满足对齐约束

自定义对齐要求

用户可以通过 basic_any 的模板参数显式指定对齐要求

using my_any = entt::basic_any<Size, Alignment>;

Size 是指定缓冲区大小 (默认是 sizeof(double[2])), Alignment 是指定缓冲区对齐方式 (默认是最严格的对齐要求)

// 自定义缓冲区大小和对齐要求
using my_any = entt::basic_any<sizeof(double[4]), alignof(double[4])>;
my_any any{42};

自动对齐检查

即使没有显式指定对齐参数, basic_any 仍会自动检查每个对象的对齐需求, 如果对象的对齐要求高于内部缓冲区的对齐能力, 小对象优化 将被禁用, 转而动态分配内存

应用场景

一些特殊类型, 例如 SIMD 类型或硬件相关的高对齐数据, 可能具有更高的对齐要求, entt::any 的这种灵活性使其能够正确处理这些类型

#include <immintrin.h> // 包含 SIMD 类型
 
using simd_any = entt::basic_any<64, alignof(__m256)>; // 缓冲区大小为 64,对齐为 SIMD 的 32 字节
 
simd_any any_simd{__m256{}};

位操作 Bit Manipulation

在性能优化领域, 位操作是一种高效的工具, 可以显著提升某些操作的效率, EnTT 提供了一些与位相关的辅助功能, 例如计算二进制位数、判断是否是 2 的幂、以及计算下一个 2 的幂, 这些功能在内存分配和算法优化中非常有用

  1. 人口计数 popcount
    计算一个无符号整数值中二进制位为 1 的个数, 这种操作在数据压缩、加密算法和其他需要统计二进制特性的场景中非常常见
    #include <entt/core/utility.hpp>
 
    unsigned int value = 42; // 42 的二进制是 101010
    std::size_t count = entt::popcount(value); // 返回 3(42 有 3 个二进制位为 1)
  1. 判断是否是 2 的幂 has_single_bit
    检查一个值是否是 2 的幂, 2 的幂在内存分配、哈希表优化等场景中非常重要
    #include <entt/core/utility.hpp>
 
    unsigned int value = 16; // 16 的二进制是 10000
    bool is_power_of_two = entt::has_single_bit(value); // 返回 true
 
    unsigned int not_power_of_two = 18; // 二进制是 10010
    bool result = entt::has_single_bit(not_power_of_two); // 返回 false
  1. 计算下一个 2 的幂 next_power_of_two
    给定一个任意值, 计算大于或等于该值的最小 2 的幂, 这在内存分配(例如页大小对齐)中非常有用
    #include <entt/core/utility.hpp>
 
    unsigned int value = 18; // 不小于 18 的最小 2 的幂是 32
    unsigned int next = entt::next_power_of_two(value); // 返回 32

高效模运算 Fast Modulus

使用 模数为 2 的幂 时, 可以用位操作代替常规的取模运算, 从而显著提高性能

常规的取模运算 (% 运算符) 通常需要执行除法操作, 而除法是一种开销较大的运算, 对于模数为 2 的幂的情况, 可以使用位操作实现等价的功能, 极大提升性能

#include <entt/core/utility.hpp>
 
std::size_t value = 123;
std::size_t modulus = 32; // 必须是 2 的幂
 
// 快速模运算
std::size_t result = entt::fast_mod(value, modulus); // 等价于 value % modulus,但更快

entt::fast_mod 通过位掩码运算 value & (modulus - 1) 替代了传统的除法操作, 从而显著提高效率

entt::fast_mod 仅适用于模数为 2 的幂的情况, 如果模数不是 2 的幂, 需使用常规的 % 运算符

压缩对 Compressed Pair

entt::compressed_pair 是 EnTT 提供的一个专注于减少内存占用的优化版本的 std::pair, 它利用了 空基类优化 Empty Base Class Optimization, EBCO 技术来最大化内存使用效率, 虽然功能上不如 std::pair 完善, 但在内存优化需求强烈的场景中是一个值得考虑的替代方案

通过利用 EBCO, 当模板参数的类型中存在空类时, 避免为该类型额外分配空间, 比如在存储某个空类和其他类型的组合时, 其总大小可能小于 std::pair

compressed_pair 的 API 非常接近 std::pair, 但去除了许多不必要的特性, 它的设计重点是性能和内存优化, 而非功能的丰富性

std::pair 的主要区别在于, firstsecond 不再是成员变量, 而是成员函数

entt::compressed_pair pair{0, 3.0};
pair.first() = 42;   // 通过函数访问 `first`
double value = pair.second();

没有 entt::make_compressed_pair, 需要显式调用构造函数来创建对象

枚举作为位掩码 Enum as Bitmask

在某些场景下, 将 enumenum class 用作位掩码是非常实用的, 例如标志管理、多状态组合等, 然而, 标准的 enum class 不支持隐式转换为底层类型, 这使得直接用作位掩码变得复杂

EnTT 提供了一种优雅的方式, 通过 模板特性 traits 支持将 enum class 作为位掩码使用, 同时避免全局作用域污染和其他潜在问题

显式注册 enum class 类型到 entt::enum_as_bitmask 特性模板中, 启用位掩码支持

#include <entt/core/utility.hpp>
 
enum class my_flag {
    unknown = 0x01,
    enabled = 0x02,
    disabled = 0x04
};
 
// 注册 my_flag
template<>
struct entt::enum_as_bitmask<my_flag>
    : std::true_type
{};

注册后, 可以直接对 my_flag 使用位运算符

int main() {
    const my_flag flags = my_flag::enabled | my_flag::disabled;
    const bool is_enabled = !!(flags & my_flag::enabled);
 
    return 0;
}

如果不能修改全局模板特性 (如第三方库的枚举), 可以通过给 enum class 添加特殊的 _entt_enum_as_bitmask 值, 自动启用位掩码支持

#include <entt/core/utility.hpp>
 
enum class my_flag {
    unknown = 0x01,
    enabled = 0x02,
    disabled = 0x04,
    _entt_enum_as_bitmask // 特殊标记值,启用位掩码支持
};

添加这个标记后, 无需显式特化 entt::enum_as_bitmask, EnTT 会自动检测并启用位掩码支持

一旦注册完成,以下常见的位运算符都可用

  • 位运算符:&|^
  • 复合赋值运算符:&=|=^=
const my_flag flags = my_flag::enabled | my_flag::disabled;
const bool has_flag = !!(flags & my_flag::enabled); // 检查是否设置了某标志

哈希字符串 Hashed String

EnTT 提供了一种轻量级的哈希字符串, 用于将可读的字符串标识符在运行时转换为数值哈希值, 这种机制兼顾了人类可读性和运行时性能, 适用于需要高效字符串比较和查找的场景

该类有一个隐式 constexpr 构造函数, 用于处理一堆字符, 创建后, 可以通过 data 成员函数获取原始字符串, 或将实例转换为数字, 散列字符串非常适合需要常量表达式的地方, 如果使用得当, 运行时不会发生字符串到数字的转换

  1. 编译期哈希 Compile-Time Hashing
    如果在编译期使用, 哈希值将在编译时计算完成, 完全不占用运行时性能, 适用于需要常量表达式 constexpr 的场景

  2. 人类可读性
    哈希字符串同时保留原始的字符串内容, 可以通过 .data() 方法获取原始字符串, 便于调试和日志记录

  3. 高效查找
    哈希值是一个 数值 hash_type, 允许快速查找和比较 (例如在哈希表中)

  4. 用户定义字面量支持
    EnTT 提供了用户定义的字符串字面量, 让代码更易读

    using namespace entt::literals;
    constexpr auto str = "text"_hs;  // 创建一个编译期哈希字符串
  1. 运行时哈希支持
    也可以在运行时创建哈希字符串或计算哈希值
    std::string orig{"text"};
    entt::hashed_string str{orig.c_str()};       // 创建哈希字符串
    const auto hash = entt::hashed_string::value(orig.c_str());  // 仅计算哈希值

编译期哈希字符串

#include <entt/core/hashed_string.hpp>
#include <iostream>
 
void load(entt::hashed_string::hash_type resource) {
    std::cout << "加载资源,哈希值为: " << resource << std::endl;
}
 
int main() {
    using namespace entt::literals;
 
    constexpr auto resource = "gui/background"_hs;  // 编译期计算哈希值
    load(resource.value());                         // 使用数值哈希值
    return 0;
}

输出

加载资源,哈希值为: 2048734533  // (示例哈希值)

获取原始字符串

#include <entt/core/hashed_string.hpp>
#include <iostream>
 
int main() {
    using namespace entt::literals;
 
    constexpr auto str = "example"_hs;
    std::cout << "原始字符串: " << str.data() << std::endl;
    std::cout << "哈希值: " << str.value() << std::endl;
 
    return 0;
}

输出

原始字符串: example
哈希值: 1234567890  // (示例哈希值)

运行时哈希字符串

#include <entt/core/hashed_string.hpp>
#include <iostream>
#include <string>
 
int main() {
    std::string orig{"runtime_string"};
    entt::hashed_string str{orig.c_str()};  // 创建运行时哈希字符串
 
    std::cout << "原始字符串: " << str.data() << std::endl;
    std::cout << "哈希值: " << str.value() << std::endl;
 
    return 0;
}

注意事项与最佳实践

  1. 避免在紧密循环中使用运行时哈希
    运行时计算哈希值会增加开销, 尤其在性能敏感的代码中, 建议使用编译期哈希

  2. 命名空间的使用
    用户定义的字面量在 entt::literals 命名空间下, 需要显式包含这个命名空间, 否则字面量无法正常使用

  3. 编译期与运行时的区分
    尽量在编译期生成哈希值 (constexpr), 仅在必要时使用运行时计算

  4. 调试与查找

    • .data() 方法可以返回原始字符串, 便于调试和日志记录

    • .value() 方法返回数值哈希, 适合高效查找和比较

宽字符 Wide Characters

EnTT 的哈希字符串不仅支持普通的字符 (char), 也支持宽字符 (wchar_t), 以便在需要处理多字节字符集 (例如 Unicode 字符串) 的场景中使用

EnTT 提供了 hashed_wstring, 它是 basic_hashed_string<wchar_t> 的别名, 专门用于宽字符表示

对于宽字符哈希字符串, 使用 _hws 作为字面量后缀

constexpr auto str = L"text"_hws;  // 宽字符编译期哈希字符串

hashed_wstringhash_type (数值哈希值) 与普通哈希字符串(hashed_string) 相同, 可以用于高效比较和查找, 提供相同的 API, 如 .data().value()

创建宽字符哈希字符串

#include <entt/core/hashed_string.hpp>
#include <iostream>
 
int main() {
    using namespace entt::literals;
 
    // 创建宽字符哈希字符串
    constexpr auto str = L"example"_hws;
 
    // 输出原始字符串和哈希值
    std::wcout << L"原始宽字符串: " << str.data() << std::endl;
    std::wcout << L"哈希值: " << str.value() << std::endl;
 
    return 0;
}

输出

原始宽字符串: example
哈希值: 1234567890  // (示例哈希值)

哈希冲突 Conflicts

在 EnTT 中, hashed_string 使用 FNV-1a 算法来对字符串进行哈希, 由于 鸽巢原理 Pigeonhole Principle, 哈希冲突是不可避免的, 这是一种普遍存在的问题

鸽巢原理 是指哈希函数将无限的字符串映射到有限的数值空间 hash_type, 因此可能存在不同的字符串映射到相同的哈希值的情况

哈希冲突无法完全避免, 与其试图完全解决冲突, 不如在发现冲突时选择不同的字符串, 如果两个字符串发生冲突, 修改其中之一, 稍作调整即可

"resource/background" -> "resource/bg"

迭代器 Iterators

EnTT 提供了一些实用工具, 专门为简化输入迭代器的编写而设计, 解决了在实现迭代器时常见的复杂性问题, 并减少了代码重复, 让迭代器的使用更加便捷和高效

输入迭代器指针 Input iterator pointer

编写自定义输入迭代器时, 需要定义诸如 value_typereferencepointer 等类型, 同时还要实现迭代器的标准操作 (如 operator*operator->), 这往往需要处理大量的样板代码, 并且当迭代器返回的是通过原地构造的值 (例如返回临时的 std::pair 对象) 时, 实现类似指针的 operator-> 行为并不容易

input_iterator_pointer 是一个轻量级的类, 用于包装原地构造的值, 并提供类似指针的行为, 它为迭代器添加了一些额外的功能, 使其更容易符合 STL 迭代器的要求

  1. 封装临时对象: 它将临时构造的值封装起来, 确保可以安全地访问其成员

  2. 支持 operator->: input_iterator_pointer 提供了一个简单的接口, 可以直接通过 operator-> 访问封装对象的成员

  3. 代码可复用性: 这一工具可在不同的迭代器实现中复用, 大大减少了重复代码

以下是一个返回 std::pair<int, std::string> 的自定义迭代器实现, 它使用了 input_iterator_pointer 来简化指针操作的处理

#include <entt/core/utility.hpp> // EnTT 的 input_iterator_pointer
#include <iterator>
#include <utility> // std::pair
 
class example_iterator {
public:
    using value_type = std::pair<int, std::string>;         // 迭代器的值类型
    using pointer = entt::input_iterator_pointer<value_type>; // 指针类型,使用 input_iterator_pointer
    using reference = value_type;                          // 引用类型
    using difference_type = std::ptrdiff_t;                // 差值类型
    using iterator_category = std::input_iterator_tag;     // 输入迭代器类型
 
    example_iterator(int start, int end)
        : current{start}, last{end} {}
 
    // 返回当前值
    reference operator*() const {
        return {current, "Value: " + std::to_string(current)};
    }
 
    // 返回指针
    pointer operator->() const {
        return pointer{**this};
    }
 
    // 前缀自增操作
    example_iterator &operator++() {
        ++current;
        return *this;
    }
 
    // 不等比较
    bool operator!=(const example_iterator &other) const {
        return current != other.current;
    }
 
private:
    int current;
    int last;
};
 

使用

#include <iostream>
#include <string>
 
int main() {
    for (example_iterator it{0, 5}; it != example_iterator{5, 5}; ++it) {
        std::cout << it->first << ": " << it->second << '\n';
    }
    return 0;
}

运行结果

0: Value: 0
1: Value: 1
2: Value: 2
3: Value: 3
4: Value: 4

整数范围迭代 Iota iterator

EnTT 中的 iota_iterator 是一个实用工具, 用于简化整数范围的迭代操作, 在等待 C++20 提供的 Rangesstd::views::iota 之前, 这个迭代器为开发者提供了一种轻量级的解决方案, 可以高效地在某个范围内生成整数序列

iota_iterator 接受一个整数起点和终点, 并在迭代时返回范围内的所有整数, 适用于需要遍历整数序列而不想为这些整数显式创建容器的情况, 特别是在处理较大范围时, 可以避免不必要的内存分配

iota_iterator 从起始值开始递增, 直到到达指定的结束值, 它只生成当前迭代到的值, 而不是预先生成整个范围的值

#include <entt/core/utility.hpp>
#include <iostream>
 
int main() {
    entt::iota_iterator first{0};   // 起始值为 0
    entt::iota_iterator last{100}; // 结束值为 100(不包括 100)
 
    for (; first != last; ++first) {
        int value = *first;  // 解引用获取当前值
        std::cout << value << " ";
    }
 
    return 0;
}

输出

0 1 2 3 4 5 ... 99

可迭代适配器 Iterable Adaptor

在 EnTT 中, 可迭代适配器 Iterable Adaptor 是一个实用工具类, 用于帮助开发者处理复杂的迭代场景, 它解决了以下问题: 当一个类提供多个迭代方法或允许对不同集合的元素进行迭代时, 如何方便地提供一个一致的接口

可迭代适配器 是一个工具类, 它接受一对迭代器 (或一个迭代器和一个终点 sentinel), 并生成一个拥有标准迭代器方法 (如 beginend) 的可迭代对象, 这让用户可以通过标准的方式访问和使用这些数据, 而不需要手动处理底层的迭代器逻辑

  1. 多种迭代方式: 当一个类支持对不同数据集或不同视角的数据进行迭代时, 可迭代适配器可以提供统一的迭代接口

  2. 视图 Views: EnTT 的视图允许用户对实体进行迭代, 但是同时也提供方法返回一个可迭代对象, 用于一次性返回 实体组件 的元组

  3. 注册表 Registry: 在 entt::registry 类中, 可迭代适配器用于返回存储中的可迭代对象, 以方便用户访问存储的元素

#include <entt/core/utility.hpp>
#include <vector>
#include <iostream>
 
int main() {
    std::vector<int> values = {1, 2, 3, 4, 5};
 
    // 使用 begin 和 end 创建一个可迭代对象
    auto iterable = entt::iterable_adaptor{values.begin(), values.end()};
 
    // 通过标准的范围 for 循环进行迭代
    for (auto value : iterable) {
        std::cout << value << " ";
    }
 
    return 0;
}

输出

1 2 3 4 5

如果你有一个类支持多种迭代方式, 可以使用可迭代适配器来提供一个标准的接口

#include <entt/core/utility.hpp>
#include <vector>
#include <iostream>
 
class MyContainer {
public:
    using iterator = std::vector<int>::iterator;
    using const_iterator = std::vector<int>::const_iterator;
 
    MyContainer() : data{1, 2, 3, 4, 5} {}
 
    // 返回可迭代对象
    auto range() {
        return entt::iterable_adaptor{data.begin(), data.end()};
    }
 
private:
    std::vector<int> data;
};
 
int main() {
    MyContainer container;
 
    // 使用 range() 方法获取可迭代对象
    for (auto value : container.range()) {
        std::cout << value << " ";
    }
 
    return 0;
}

输出

1 2 3 4 5

在 EnTT 的视图中, 可迭代适配器被广泛用于同时返回 实体组件 的元组

#include <entt/entt.hpp>
#include <iostream>
 
int main() {
    entt::registry registry;
 
    // 创建一些实体并附加组件
    auto entity1 = registry.create();
    auto entity2 = registry.create();
    registry.emplace<int>(entity1, 42);
    registry.emplace<int>(entity2, 84);
 
    // 获取视图
    auto view = registry.view<int>();
 
    // 使用可迭代对象进行迭代
    for (auto [entity, component] : view.each()) {
        std::cout << "Entity: " << entt::to_integral(entity) 
                  << ", Component: " << component << "\n";
    }
 
    return 0;
}

输出

Entity: 0, Component: 42
Entity: 1, Component: 84

这里的 view.each() 返回了一个可迭代对象, 允许用户以元组的形式访问实体及其组件

单态 Monostate

Monostate 模式在 EnTT 中是一个轻量级且线程安全的方式, 用于管理全局或共享状态, 是传统 单例模式Singleton 的一个替代方案, 它特别适合用于配置或共享数据的场景, 避免了单例模式常见的问题 (如全局访问难以控制、单元测试不方便等)

  1. 键值关联:

    • 键是 整型值, 通常通过字符串哈希得到 (例如使用 entt::hashed_string_hs 字面量)

    • 值是 基础类型, 如 intboolfloat, 同一个键可以存储不同类型的值

  2. 类型安全:
    访问数据时, 读取的类型必须与存储时的类型一致, 如果类型不匹配, 可能导致意想不到的结果, 因为每种类型的值是独立存储的

  3. 线程安全:
    Monostate 的实现本身是线程安全的, 非常适合在多线程环境中使用

  4. 无需显式实例化:
    Monostate 不需要显式创建实例, 数据是静态管理的, 通过模板键直接访问

#include <entt/core/hashed_string.hpp>
#include <entt/core/monostate.hpp>
#include <iostream>
 
int main() {
    // 用相同的键存储不同类型的值
    entt::monostate<entt::hashed_string{"mykey"}>{} = true;
    entt::monostate<"mykey"_hs>{} = 42;
 
    // 读取值
    const bool b = entt::monostate<entt::hashed_string{"mykey"}>{}; // 获取布尔值
    const int i = entt::monostate<"mykey"_hs>{};                    // 获取整型值
 
    // 输出读取的值
    std::cout << "布尔值: " << std::boolalpha << b << '\n';
    std::cout << "整型值: " << i << '\n';
 
    return 0;
}

类型支持 Type support

EnTT 提供了有关所有类型的一些基本信息, 它还提供了标准库中尚未提供或永远不会提供的附加功能

内置 RTTI 支持

EnTT 提供了一套功能强大的类型支持机制, 包括内置 RTTI 运行时类型识别 支持, 可以在禁用 C++ 标准 RTTI 的情况下提供类似功能

类型的唯一序列化标识符

EnTT 提供 entt::type_index 来生成类型的唯一序列化标识符

auto index = entt::type_index<a_type>::value();
  • 返回值: 返回值是类型的唯一序列化标识符, 可以用于关联容器 (如 unordered_map) 或位置访问(如 vector)

  • 不稳定性: 标识符在不同程序运行中不保证稳定

  • 可定制: type_indexSFINAE 友好的, 可以通过特化为类型自定义生成逻辑

    template<typename Type>
    struct entt::type_index<Type, std::void_t<decltype(Type::index())>> {
        static entt::id_type value() noexcept {
            return Type::index();
        }
    };

如果自定义逻辑, 必须确保标识符是顺序生成的, 否则会破坏假设, 导致不可预知行为

类型的哈希值

EnTT 提供 entt::type_hash 来生成类型的哈希值

auto hash = entt::type_hash<a_type>::value();
  • 编译期常量: 通常情况下, 该函数是 constexpr, 但不保证所有编译器和平台都支持 (最流行的平台都支持)

  • 稳定性: 默认情况下, 哈希值在不同运行中保持稳定, 但如果使用宏定义 ENTT_STANDARD_CPP, 哈希值在运行时生成, 且不再稳定, 但是将不再使用语言的非标准特性

  • 可定制: type_hash 同样支持 SFINAE, 可以特化以提供自定义行为

类型名称

EnTT 提供 entt::type_name 来提取类型名称

auto name = entt::type_name<a_type>::value();

返回值是一个 std::string_view, 通常在编译期生成

名称提取依赖于 编译器

struct my_type { /* ... */ };
  • GCC/Clang 中, 返回 my_type
  • MSVC 中, 返回 struct my_type

可以通过对返回值进行额外处理来移除 struct 等修饰符, 如果定义了宏 ENTT_STANDARD_CPP, 则无法使用非标准特性, 可能导致返回值为空, type_name 也是 SFINAE 友好的, 可以特化以自定义行为

类型信息 type_info

EnTT中 的 type_info 类并不是 std::type_info 的直接替代品, 但它可以提供类似的信息, 且这些信息不依赖于具体实现, 也不需要启用 RTTI

EnTT 中的 type_info 类型定义了一个不透明的类, 它是可以拷贝和移动的

// by type
auto info = entt::type_id<a_type>();
 
// by value
auto other = entt::type_id(42);

返回的 type_info 对象, 本质上是对某些具有 静态存储期type_info 实例的 常量引用

静态存储期 的对象是在程序生命周期内一直存在的对象, 它们只会被分配一次并且不会被释放, 这意味着这些对象的内存空间在程序的整个运行期间是固定的

如果需要存储某个类型的 type_info, 只需要保存这个引用, 而无需拷贝整个 type_info 对象, 引用本身的内存开销与指针类似, 仅占用少量内存, 因此, 这种方式既节省了内存, 又提供了高效的类型信息访问

// 获取类型信息
auto info1 = entt::type_id<int>();
auto info2 = entt::type_id<int>();
 
// 比较两个类型信息
assert(&info1 == &info2); // 返回的是对同一静态实例的引用
 
// 使用info保存类型元信息
std::cout << "Hash: " << info1.hash() << ", Name: " << info1.name() << "\n";

在上面的代码中, entt::type_id<int>() 每次返回的都是对同一个 type_info 实例的引用, 即使我们多次调用 entt::type_id<int>(), 内存中也只有一个 type_info 实例, 这就体现了 静态存储期高效内存利用 的设计优势

当然, 如果不希望如此, 也可以执行直接构造对象

entt::type_info info{std::in_place_type<int>};

以下信息由 type_info 提供

  1. 类型的索引
    每个类型都有一个唯一的哈希值
    auto idx = entt::type_id<a_type>().index();
    // 或者等价的别名
    auto idx =  entt::type_index<std::remove_cv_t<std::remove_reference_t<a_type>>>::value();

这里 std::remove_cv_tstd::remove_reference_t 用于去除类型的 constvolatile 和引用修饰符

  1. 类型的哈希值
    每个类型都有一个唯一的哈希值
    auto hash = entt::type_id<a_type>().hash();
    // 等价别名
    auto hash = entt::type_hash<std::remove_cv_t<std::remove_reference_t<a_type>>>::value();
  1. 类型的名称
    获取类型的名称 (通常是类型的字符串表示)
    auto name = entt::type_id<my_type>().name();
    // 等价别名
    auto name = entt::type_name<std::remove_cv_t<std::remove_reference_t<a_type>>>::value();

如果所使用的任何功能都保证在 编译时可用, 那么 type_info 也是完全可用的 constexpr, 但是这无法提前保证, 要取决于所使用的 编译器 和上述类的任何特化

潜在冲突

EnTT 的 类型哈希 type_hash 是通过对类型名称进行哈希计算来生成的, 这种实现方式非常高效, 特别是在编译期即可完成, 但由于哈希本质上是将无限的输入映射到有限的输出, 理论上可能会出现哈希冲突

  1. 哈希冲突
    两个不同的类型可能被分配相同的哈希值 (尽管概率极低), 原因在于 type_hash 是基于字符串哈希计算的, 两个不同的类型名可能映射到相同的哈希值

  2. 跨上下文类型名称重复
    如果两个运行时动态加载的库中有完全相同的类型名称 (如两个库中都定义了 namespace foo::MyClass), 那么它们的类型名称 (type_name) 会相同, 从而导致它们的哈希值也相同

解决方法

  1. 定义宏 ENTT_STANDARD_CPP
    如果定义了 ENTT_STANDARD_CPP 宏, EnTT 会使用运行时生成的标识符, 而不是编译期生成的标识符, 运行时标识符不依赖于字符串哈希, 因此避免了冲突问题, 但是如果使用 插件系统 (例如动态加载的库), 这种方式可能无法很好地工作, 因为插件之间的标识符可能仍会冲突

  2. 特化 type_name
    对于冲突的类型, 可以特化 type_name, 为它们指定一个唯一的标识符

    namespace entt {
        template<>
        struct type_name<foo::MyClass> {
            static constexpr std::string_view value() noexcept {
                return "unique_foo_MyClass";
            }
        };
    }

这种方法简单直接, 同时保留了 type_hash 的性能和功能

  1. 自定义标识符生成策略
    可以实现完全自定义的标识符生成策略, 例如, 使用枚举类来为每个类型生成唯一标识符, 在预处理步骤中为每个类型生成哈希值, 确保唯一性

在大多数情况下, 冲突几乎不会发生, 如果不涉及动态加载的库或插件系统, 可以放心使用默认实现, 即使出现冲突, 也允许用户根据需求调整标识符生成逻辑, 可以很容易地解决问题

类型特征 Type traits

EnTT 类型特性 type traits 模块提供了一些工具, 这些工具可以极大地增强 C++ 的类型系统, 同时为模板元编程提供高效的解决方案


size_of

提供一种替代 sizeof 的方法, 用于在所有情况下计算类型大小, 即使是函数类型或不完整类型, 如果类型不支持, 则返回零

const auto size = entt::size_of_v<void>;  // 返回 0,因为 void 类型无大小

提高对不完整类型或特殊类型 (如 void) 的容错性, 避免编译错误


is_applicable

entt::is_applicable 是对标准库 std::is_invocable 的一种扩展, 它允许检查一个可调用对象是否能通过 元组类型 的参数调用, 它的主要功能是解包元组, 并简化了调用点的代码, 这种工具特别适用于元编程中需要检查函数或可调用对象兼容性的场景

constexpr bool result = entt::is_applicable<Func, std::tuple<a_type, another_type>>;

基于 std::is_invocable, 自动解包元组形式的参数, 简化代码书写, 判断函数或可调用对象是否支持特定形式的调用, 尤其是参数是通过元组传递的情况下


constness_as

用于将一个类型的 const 属性传递给另一个类型

using type = entt::constness_as_t<dst_type, const src_type>;  // 如果 src_type 是 const,dst_type 也会变为 const

仅适用于 非引用类型, 对 引用类型 传递 const 属性时, 可能无法获得预期结果, 在元编程中需要动态传递 const 属性时非常方便


member_class_t

用于从类成员中提取其所属的类类型

template<typename Member>
using clazz = entt::member_class_t<Member>;

当使用 auto 模板参数使类成员类型变得模糊时, 可以用这个工具快速提取其所属的类


nth_argument_t

用于提取函数、成员函数或数据成员的第 N 个参数类型

using type = entt::nth_argument_t<1u, decltype(&clazz::member)>;

提供对不透明类型的快速操作能力, 如果有重载函数, 用户需自行解决歧义问题


integral_constant

entt::integral_constantstd::integral_constant 的简化版本, 用于创建类型和值的常量

constexpr auto constant = entt::integral_constant<42>;  // 定义值为 42 的常量

还可以结合哈希字符串, 创建可读性更强的标签

constexpr auto enemy_tag = entt::integral_constant<"enemy"_hs>;
registry.emplace<enemy_tag>(entity);

tag

entt::tag 是基于 id_type 的快捷方式, 用于创建人类可读的标记 (类似于标签)

registry.emplace<entt::tag<"enemy"_hs>>(entity);

可以使用任何可转换为 id_type 的值, 包括未限定的枚举


type_listvalue_list

提供了类型列表和值列表的工具, 这是元编程中不可或缺的特性, 它们是复杂类型元编程的核心工具, 可用于简化类型操作、类型转换和类型推导

获取 类型/值 列表的第 N 个元素

using type = entt::type_list_element<2, entt::type_list<int, float, double>>;
 

获取特定类型的索引

constexpr auto index = entt::type_list_index<int, entt::type_list<int, float, double>>::value;

连接类型列表

using list = entt::type_list_cat<entt::type_list<int>, entt::type_list<float, double>>;

去除重复类型

using unique_list = entt::type_list_unique<entt::type_list<int, float, int, double>>;

判断是否包含某类型

constexpr bool contains = entt::type_list_contains<int, entt::type_list<float, int>>::value;

从类型列表中移除特定类型

using diff_list = entt::type_list_diff<entt::type_list<int, float>, entt::type_list<float>>;

类型转换

using transformed_list = entt::type_list_transform<std::add_pointer, entt::type_list<int, float>>;

以上操作同样适用于 值列表

constexpr auto index = entt::value_list_index<42, entt::value_list<10, 20, 42>>::value;

唯一顺序标识符 Unique sequential identifiers

在 C++ 中, 有时需要为类型分配唯一的、顺序的数字标识符, 无论是在 编译期 还是 运行时, EnTT 提供了强大的工具(如 identfamily), 专门用于满足这种需求, 这些工具充分利用了现代 C++ 的特性

编译期生成器 ident

entt::ident 是一个模板类, 可以在编译时为类型生成唯一的数字标识符

  • 为每种类型生成唯一的、稳定的标识符
  • 生成的标识符是常量表达式 constexpr, 可以在编译期使用
  • 即使类型列表发生变化 (例如移除某些类型), 标识符的稳定性也可以通过占位类型 ignore_type 保证
#include <iostream>
#include <type_traits>
#include <entt/core/ident.hpp>
 
// 定义类型标识符
using id = entt::ident<int, double, char>;
 
int main() {
    // 获取类型对应的标识符
    constexpr auto int_id = id::value<int>;
    constexpr auto double_id = id::value<double>;
    constexpr auto char_id = id::value<char>;
 
    // 打印标识符
    std::cout << "int_id: " << int_id << '\n';       // 输出: 0
    std::cout << "double_id: " << double_id << '\n'; // 输出: 1
    std::cout << "char_id: " << char_id << '\n';     // 输出: 2
 
    // 使用标识符的上下文(例如 switch-case)
    switch (char_id) {
    case id::value<int>:
        std::cout << "Type is int\n";
        break;
    case id::value<double>:
        std::cout << "Type is double\n";
        break;
    case id::value<char>:
        std::cout << "Type is char\n";
        break;
    default:
        std::cout << "Unknown type\n";
    }
}

占位类型的使用

如果需要移除某些类型但保持标识符的稳定性, 可以使用 ignore_type

template<typename> struct ignore_type {};
 
using id = entt::ident<
    int,
    ignore_type<float>,  // 占位,确保其他标识符不变
    double
>;
 
constexpr auto int_id = id::value<int>;     // 0
constexpr auto double_id = id::value<double>; // 2

如此一来, 在生产环境中, 标识符不会因为某些类型的移除而改变, 并且这一切完全在编译期完成, 零运行时开销

运行时生成器 family

entt::family 是一个模板类, 可以在运行时为类型生成唯一的数字标识符

  • 标识符是通过运行时的调用顺序生成的
  • 每个类型都有一个唯一的标识符, 但标识符的值不稳定, 不同运行可能生成不同的值
  • 可以通过自定义标签生成不同的标识符序列, 支持多种用途
#include <iostream>
#include <entt/core/family.hpp>
 
// 定义类型标识符生成器
using id = entt::family<struct my_tag>;
 
int main() {
    // 获取类型的运行时标识符
    const auto int_id = id::value<int>;
    const auto double_id = id::value<double>;
 
    // 打印标识符
    std::cout << "int_id: " << int_id << '\n';       // 可能输出: 0
    std::cout << "double_id: " << double_id << '\n'; // 可能输出: 1
 
    // 注意:运行时标识符值可能在不同运行之间变化
}

标识符的生成取决于执行顺序

自定义标签

通过自定义标签, 可以为不同的场景生成独立的标识符序列

using physics_id = entt::family<struct physics_tag>;
using graphics_id = entt::family<struct graphics_tag>;
 
const auto physics_int_id = physics_id::value<int>;
const auto graphics_int_id = graphics_id::value<int>;
 
// 同一个类型在不同标签下的标识符互不干扰
std::cout << physics_int_id << '\n'; // 0
std::cout << graphics_int_id << '\n'; // 0

支持动态标识符生成, 适合运行时需要动态扩展的场景, 通过标签可以生成独立的标识符序列

比较:编译期 vs 运行时

特性编译期生成器 (ident)运行时生成器 (family)
生成时间编译时运行时
标识符稳定性稳定不稳定
性能零运行时开销存在运行时开销
灵活性只能处理已知类型支持动态扩展
用途模板元编程、常量表达式动态类型注册、插件系统

实用工具 Utilities

以下是 EnTT 提供的一些实用工具的介绍和解析, 这些工具的设计目标是简化开发工作, 尤其是元编程和函数操作中的复杂任务

entt::identity

entt::identity 是一个简单的标识函数对象, 接受一个参数并原样返回, 不进行任何操作, 可以作为模板元编程中的 无操作 占位函数, 在需要一个默认操作但不希望修改输入的场景中非常有用

#include <entt/core/utility.hpp>
 
int main() {
    int value = 42;
    int result = entt::identity{}(value);  // 返回原值 42
    return 0;
}

entt::overload

entt::overload 用于从重载函数中选择正确的版本, 支持自由函数和成员函数, 通过指定函数的类型, 消除重载的歧义, 简化代码书写

#include <entt/core/utility.hpp>
 
struct clazz {
    void bar(int) {}
    void bar() {}
};
 
int main() {
    // 从重载中选择 void(int) 版本
    auto *member = entt::overload<void(int)>(&clazz::bar);
 
    // 等价于手动写的类型转换
    auto *equivalent = static_cast<void(clazz::*)(int)>(&clazz::bar);
    return 0;
}

提高可读性, 减少手动类型转换的繁琐

entt::overloaded

entt::overloaded 是一个小型类模板, 用于从一组 Lambda 或仿函数中创建一个支持多种调用类型的 operator(), 这在需要传递支持多种类型调用的可调用对象时非常有用, 例如在元编程中, 避免手动定义多个 operator() 的繁琐

#include <entt/core/utility.hpp>
#include <iostream>
 
int main() {
    // 定义支持多种类型的可调用对象
    entt::overloaded func{
        [](int value) { std::cout << "int: " << value << '\n'; },
        [](char value) { std::cout << "char: " << value << '\n'; }
    };
 
    func(42);   // 输出: int: 42
    func('c');  // 输出: char: c
 
    return 0;
}

简化了多种类型调用场景, 尤其适用于 std::visit 或其他元编程场景

entt::y_combinator

entt::y_combinatorY 组合子在 C++ 中的实现, 它允许在没有显式定义递归函数的情况下实现递归调用, 适合需要递归但又不想定义全局递归函数时, 例如在模板元编程或匿名递归计算场景中

#include <entt/core/utility.hpp>
#include <iostream>
 
int main() {
    // 使用 Y 组合子实现递归计算高斯和
    entt::y_combinator gauss([](const auto &self, auto value) -> unsigned int {
        return value ? (value + self(value - 1u)) : 0;
    });
 
    const auto result = gauss(3u);  // 计算 3 + 2 + 1 + 0 = 6
    std::cout << "Result: " << result << '\n';  // 输出: Result: 6
 
    return 0;
}

这初看可能显得复杂, 但对于动态递归调用场景非常高效, 避免了显式定义递归函数的额外代码开销

Y 组合子 Y Combinator

Y 组合子固定点组合子 的一种, 它是一个高阶函数, 主要用于实现递归函数的定义, 尤其是在没有显式命名的情况下实现匿名递归, 简单来说, Y 组合子允许你在函数内部递归调用自身, 而不需要显式地给函数起一个名字, 这在函数式编程中非常有用, 因为有些语言 (例如 Lambda 演算或某些纯函数式语言) 中没有直接的递归定义机制

Lambda 演算中,Y 组合子的定义如下

Y = λf.(λx.f (x x)) (λx.f (x x))

它的核心思想是: 将一个函数 f 转换为可以自我调用的递归函数, Y 本质上是通过传递自身作为参数, 让函数可以间接调用自己