EnTT: 运行时反射系统 Runtime reflection system

整理 EnTT meta 运行时反射系统中的标识符、meta_any、容器、指针类型、自动转换、策略和上下文。

反射 Reflection 是一种编程技术, 指程序在运行时能够自省 introspection 和操作自身的结构和行为, 这种能力使程序可以动态地获取类型信息、访问对象的成员, 甚至是修改类和方法的行为, 而不需要在编译时显式地知道这些信息, EnTT 提供了 entt::meta 作为运行时反射系统

名称和标识符 Names and identifiers

在 entt::meta 中, 标识符 用于关联 元对象 meta objects 以便后续引用和操作, 然而, EnTT 并不强制用户必须使用库内提供的工具 (如 hashed_string) 来生成这些 标识符, 元系统 允许用户使用任意的 数值类型标识符 numeric identifiers, 只要 标识符 是 数值类型 (如整数, 无符号整数), 那么不管它是在运行时生成、编译时生成, 或者通过自定义函数生成, 都是可以的

  • 运行时生成: 例如,通过动态分配整数值作为标识符
  • 编译时生成: 通过编译期计算(如字符串哈希)
  • 自定义函数生成: 使用用户定义的逻辑生成标识符

基于 hashed_string 的标识符

entt::meta_factory<my_type>{}.type("reflected_type"_hs);

这里的 "reflected_type"_hs 是通过 hashed_string 类生成的标识符

  • "reflected_type" 是人类可读的字符串
  • _hs 是 hashed_string 的用户定义字面量, 用于在编译期生成标识符

直接使用数值标识符

entt::meta_factory<my_type>{}.type(42u);

此处, 42u 是一个直接的数值标识符, 尽管功能上与 hashed_string 的生成结果等价, 但它不可读, 不利于调试和维护

反射系统 Reflection

在 EnTT 中, 反射是一种将 C++ 类型及其成员、行为暴露给运行时系统的方法, 从而支持动态交互和类型检查

反射始终从 实际存在的 C++ 类型 开始, 不能对不存在的类型 (如 虚拟类型) 进行反射, 创建反射的起点是 meta_factory

entt::meta_factory<my_type> factory{};

meta_factory 是一个构建器, 用于定义元类型及其关联的功能 (构造函数、数据成员等), 它提供了一种 链式调用 的 API, 便于高效地配置反射类型

默认情况下, EnTT 会为每种类型分配一个标识符, 通过其内置的运行时类型识别系统生成, 也可以手动为类型分配自定义标识符

entt::meta_factory<my_type>{}.type("reflected_type"_hs);

标识符的作用是允许用户在运行时通过名字而非 C++ 类型访问元类型, 如果不需要通过标识符搜索该类型, 可以不调用 .type() 方法

链式 API

meta_factory 提供链式调用方式, 允许用户轻松定义类型的各种功能, 以下是反射系统支持的主要功能

构造函数 Constructors

可以通过指定参数列表为元类型定义构造函数, 支持实际构造函数和自由函数 (free function)

entt::meta_factory<my_type>{}
    .ctor<int, char>()  // 定义 (int, char) 类型的构造函数
    .ctor<&factory>();  // 将自由函数视为构造函数

如果可能, 系统会自动生成默认构造函数

析构函数 Destructors

可以使用自由函数或成员函数作为析构函数

entt::meta_factory<my_type>{}.dtor<&destroy>();

注意, 析构函数的作用是执行特殊的资源清理工作, 不应直接删除对象或显式调用其析构函数

数据成员 Data Members

数据成员包括:

  • 实例成员变量
  • 静态变量或全局变量
  • 任意类型的常量
entt::meta_factory<my_type>{}
    .data<&my_type::static_variable>("static"_hs)  // 静态变量
    .data<&my_type::data_member>("member"_hs)      // 成员变量
    .data<&global_variable>("global"_hs);          // 全局变量

.data() 方法需要指定标识符 (如 "member"_hs), 运行时可通过名字访问

可以通过 getter 和 setter 函数来定义数据成员, 这些 getter 和 setter 函数可以是

  • 自由函数: 独立于类的函数
  • 类成员函数: 属于类的成员函数
  • 二者的混合: getter 和 setter 分别使用自由函数或成员函数的组合
int get_data();
void set_data(int value);
 
// 注册为反射系统中的数据成员
entt::meta_factory<my_type>{}.data<&set_data, &get_data>("data"_hs);

可以利用这点可以为非 const 成员变量定义只读属性, 通过只设置 getter 函数而不设置 setter 函数来实现

entt::meta_factory<my_type>{}.data<nullptr, &my_type::data_member>("member"_hs);

支持通过 entt::value_list 为成员变量定义多个 setter

entt::meta_factory<my_type>{}
    .data<
        entt::value_list<&from_int, &from_string>, 
        &my_type::data_member
    >("member"_hs);

成员函数 Member Functions

可以将成员函数和自由函数关联到元类型

entt::meta_factory<my_type>{}
    .func<&my_type::static_function>("static"_hs)  // 静态函数
    .func<&my_type::member_function>("member"_hs)  // 成员函数
    .func<&free_function>("free"_hs);              // 自由函数

.func() 方法需要指定标识符 (如 "member"_hs), 支持函数重载, 反射系统会在运行时根据参数类型解析重载的函数

基类 Base Classes

如果类型从另一个类型派生, 可以在反射系统中定义这种继承关系

entt::meta_factory<derived_type>{}.base<base_type>();

系统会跟踪基类和派生类之间的关系, 并支持运行时的隐式类型转换, 即需要 base_type 的地方, 也可以使用 derived_type 的实例

转换函数 Conversion Functions

可以通过转换函数来链接用户定义的类型之间的隐式转换

entt::meta_factory<double>{}.conv<int>();

允许反射系统在运行时根据需要隐式转换类型

meta_any

meta_any 是 EnTT 反射系统中对 entt::any 的扩展版本, 旨在与元类型系统深度集成, 提供额外的功能,而无需重复实现底层存储逻辑

meta_any 的 API 与 entt::any 类似, 可以动态存储任意类型的对象, 它在 entt::any 的基础上添加了对 容器 和 类似指针类型 pointer-like types 的支持, 这些功能在 entt::any 中是没有的

meta_any 可与元类型系统无缝结合, 支持基于元数据的功能, 例如动态反射和类型推导, meta_any 能够推断出 元节点 meta node, 然后将参数转发给底层存储器

对空实例的处理

meta_any 和 entt::any 对空实例的定义不同

  • entt::any: 无论是空构造 (any{}) 还是显式构造 void 类型 (any{std::in_place_type<void>}), 都被视为 空 实例

  • meta_any: meta_any 被视为 空, meta_any{std::in_place_type<void>} 则被视为有效的对象

这种差异的目的是为了支持更细粒度的状态管理, 区分函数调用失败和返回 void 的成功调用

entt::meta_any empty{};
entt::meta_any other{std::in_place_type<void>};
 
std::cout << empty ? "Valid" : "Empty";  // 输出:Empty
std::cout << other ? "Valid" : "Empty";  // 输出:Valid

创建 meta_any

类似于 entt::any, 可以直接构造对象或从已有对象创建动态对象 meta_any

entt::meta_any instance = 42;  // 存储一个整数

引用未管理对象

可以通过 std::in_place_type<T &> 或 forward_as_meta 来创建对未管理对象的引用

int value = 42;
entt::meta_any ref = std::forward_as_meta(value);  // 引用

接管指针的所有权

可以通过 std::in_place 将动态分配的对象的所有权转移到 meta_any

entt::meta_any ptr{std::in_place, new int{42}};

类型转换

meta_any 提供了以下类型转换方法

  • try_cast: 尝试将存储的对象转换为目标类型, 如果失败返回 nullptr
  • cast: 强制转换存储的对象为目标类型 (抛出异常或断言失败时)
  • allow_cast: 转换对象使其可以被其他类型的引用接收

事实上, meta_any 不存在 any_cast

运行时使用反射

一旦构建好了反射类型的关系网,就可以在运行时通过反射系统进行类型的查询、操作和实例化

访问反射类型

反射系统提供了多种方式来查询类型

// direct access to a reflected type
auto by_type = entt::resolve<my_type>();
 
// look up a reflected type by identifier
auto by_id = entt::resolve("reflected_type"_hs);
 
// look up a reflected type by type info
auto by_type_id = entt::resolve(entt::type_id<my_type>());

还存在一个函数重载 resolve, 用于一次性迭代所有反射类型, 它返回一个可迭代对象, 用于 range-for 循环

for (auto &&[id, type] : entt::resolve()) {
    // 处理每个类型的 id 和 type
}

在所有情况下, 返回的值都是 meta_type 的一个实例 (可能带有其 id), 此类对象提供 API 来了解其运行时标识符, 迭代与其关联的所有元对象, 甚至构建底层类型的实例


元数据成员 meta Data Members

可以通过标识符访问数据成员

auto data = entt::resolve<my_type>().data("member"_hs);

返回类型是 entt::meta_data, 可以用于查询数据成员是否为 const 或 static, 或者获取并设置数据成员的值

if (data) {
    // 获取值
    auto value = data.get(instance).cast<int>();
 
    // 设置值
    data.set(instance, 42);
}

元成员函数 meta Member Functions

也可以通过标识符访问成员函数

auto func = entt::resolve<my_type>().func("member"_hs);

返回类型是 entt::meta_func, 可以查询函数是否为 const 或 static, 或者获取函数的参数个数、返回值类型和参数类型

// 调用函数
if (func) {
    auto result = func.invoke(instance, arg1, arg2);
    if (result) {
        auto return_value = result.cast<int>();
    }
}

这样获得的所有 元对象 meta_data 以及 元类型 meta_func 都可以明确转换为布尔值以检查有效性

if (auto func = entt::resolve<my_type>().func("member"_hs); func) {
    // 函数有效,可以调用
}

类型继承和关系

访问基类

可以通过 base() 获取类型的基类信息

for (auto &&[id, type] : entt::resolve<my_type>().base()) {
    // 处理基类类型
}

隐式类型转换

反射系统会跟踪类型之间的继承关系, 支持运行时的隐式类型转换, 例如, 可以将派生类型的实例用作基类


动态实例化和销毁

创建实例

通过反射系统可以动态创建类型实例, 使用 construct() 方法并传递构造函数所需的参数

auto instance = entt::resolve<my_type>().construct(arg1, arg2);
if (instance) {
    // 实例化成功
    auto obj = instance.cast<my_type>();
}

返回类型是 entt::meta_any, 如果找不到匹配的构造函数, 返回的 meta_any 将是无效的

析构函数

反射系统不直接暴露析构函数接口, 对象的析构由 meta_any 自动管理, 无需用户显式调用


转换函数

转换函数在反射系统中是隐式的, 用户无法直接访问, 它们在需要类型转换时由 meta_any 或其他元对象自动调用

容器支持 Container

EnTT 的运行时反射系统支持各种容器, 包括标准库提供的容器和用户自定义的容器

内置支持

EnTT 已经为以下常见容器提供了支持

  • 序列容器: std::vector、std::array、std::deque 和 std::list (不包括 std::forward_list)
  • 关联容器: std::map、std::set 及其无序版本 (std::unordered_map、std::unordered_set)

在使用这些容器时, 确保包含 container.hpp 头文件, 其中定义了这些容器的特化实现, 同时, container.hpp 文件还提供了自定义容器支持的示例代码

自定义容器支持

如果想让自定义容器能够被 EnTT 的元系统识别, 需要为以下类之一进行特化

  1. meta_sequence_container_traits: 用于序列容器
  2. meta_associative_container_traits: 用于关联容器

特化后, EnTT 会将自定义容器视为标准容器, 并提供统一的接口操作

#include <entt/meta/meta.hpp>
 
template<>
struct entt::meta_sequence_container_traits<MyCustomContainer> {
    static size_type size(const MyCustomContainer &container) {
        return container.size();
    }
    static iterator begin(MyCustomContainer &container) {
        return container.begin();
    }
    static iterator end(MyCustomContainer &container) {
        return container.end();
    }
    // 可添加额外的自定义逻辑
};

使用容器的代理对象

EnTT 提供了 代理对象 proxy object 来操作容器, 这些代理对象通过 meta_any 类提供

  • 序列容器: 调用 as_sequence_container() 获取代理对象
  • 关联容器: 调用 as_associative_container() 获取代理对象

代理对象可以用来遍历或修改容器中的数据

#include <entt/entt.hpp>
#include <vector>
#include <iostream>
 
int main() {
    std::vector<int> vec{1, 2, 3};
    entt::meta_any any = entt::forward_as_meta(vec);
 
    // 检查是否是序列容器
    if(any.type().is_sequence_container()) {
        // 获取代理对象
        if(auto view = any.as_sequence_container(); view) {
            std::cout << "容器大小:" << view.size() << "\n";
            for(auto &&elem : view) {
                std::cout << "元素值:" << elem.cast<int>() << "\n";
            }
        }
    }
 
    return 0;
}

这是一个冗长的例子, 实际上并不需要进行双重检查

#include <entt/entt.hpp>
#include <vector>
#include <iostream>
 
int main() {
    std::vector<int> vec{1, 2, 3};
    entt::meta_any any = entt::forward_as_meta(vec);
 
    // 方式 1:直接检查类型
    if(any.type().is_sequence_container()) {
        auto view = any.as_sequence_container();
        if(view) { // 验证代理对象是否有效
            std::cout << "代理对象有效,容器大小:" << view.size() << "\n";
        }
    }
 
    // 方式 2:不检查类型,直接验证代理对象有效性
    if(auto view = any.as_sequence_container(); view) {
        std::cout << "代理对象有效,容器大小:" << view.size() << "\n";
    }
 
    // 如果封装的对象不是容器
    int nonContainer = 42;
    entt::meta_any nonContainerAny = entt::forward_as_meta(nonContainer);
 
    if(auto invalidView = nonContainerAny.as_sequence_container(); !invalidView) {
        std::cout << "无效的代理对象,非容器类型。\n";
    }
 
    return 0;
}

序列容器 Sequence Containers 的代理对象接口

  1. value_type
    返回容器元素的元类型 meta_type

  2. size
    返回容器中元素的数量, 类型为无符号整数

  3. resize
    修改容器大小并返回 true 表示成功, 对于固定大小的容器 (如 std::array), 此操作将失败

  4. clear
    清空容器并返回 true 表示成功, 固定大小的容器无法执行此操作

  5. reserve
    预分配容器容量,返回 true 表示成功, 固定大小的容器无法执行此操作

  6. begin 和 end
    返回容器的迭代器, 可以用于直接遍历容器

    for(entt::meta_any element : view) {
        // 处理元素
    }
  1. insert
    在指定位置插入元素
    auto last = view.end();
    view.insert(last, 42); // 在末尾插入整数 42

返回插入位置的迭代器以及一个布尔值, 表示操作是否成功, 固定大小容器或类型转换失败时会返回无效的结果

  1. erase
    删除指定位置的元素
    auto first = view.begin();
    view.erase(first); // 删除第一个元素

返回指向下一个元素的迭代器, 以及一个布尔值, 表示操作是否成功

  1. operator[]
    按位置访问容器中的元素
    for(std::size_t pos = 0; pos < view.size(); ++pos) {
    entt::meta_any value = view[pos];
        // 处理元素
    }

返回的是一个直接引用容器中实际元素的 meta_any, 修改返回的对象会直接修改容器中的元素, 注意, 对于如 std::list 这样的容器, 位置访问效率较低, 因为需要线性遍历

关联容器 Associative Containers 的代理对象接口

  1. key_only
    如果容器是只包含键的 (如 std::set), 返回 true

  2. key_type
    返回键的元类型 meta_type

  3. mapped_type

  • 对于只包含键的容器 (如 std::set), 返回无效的 meta_type
  • 对于键值对容器 (如 std::map), 返回值的 元类型
  1. value_type
    返回容器元素的元类型
  • 对于 std::set<int>, 返回 int
  • 对于 std::map<int, char>, 返回 std::pair<const int, char>
  1. size
    返回容器中元素的数量

  2. clear
    清空容器, 返回 true 表示成功

  3. reserve
    预分配容量, 返回 true 表示成功 (不适用于标准映射类型)

  4. begin 和 end
    返回容器的迭代器, 可以用于直接遍历容器:

    for(std::pair<entt::meta_any, entt::meta_any> element : view) {
        // element.first 是键
        // element.second 是值
    }

对于只包含键的容器, element.second 是一个无效的 meta_any

  1. insert
    插入键值对
    view.insert(view.end().handle(), 42, 'c'); // 插入键 42 和值 'c'

返回一个布尔值, 表示插入是否成功, 类型转换失败时插入操作会失败

  1. erase
    根据键删除元素
    view.erase(42); // 删除键为 42 的元素

返回布尔值,表示删除是否成功

  1. operator[]
    按键访问值
    entt::meta_any value = view[42];

返回一个直接引用容器中实际元素的 meta_any, 修改返回的对象会直接修改容器中的元素

类指针类型 Pointer-like types

EnTT 的元系统支持指针类型, 这使得用户可以通过 meta_any 解引用指针类型的实例, 从而获取与其元类型正确关联的轻量级引用, 这种设计允许用户直接修改被指针指向的对象, 或者通过扩展支持自定义指针类型

默认支持以下指针类型:

  1. 原生指针 raw pointers: 如 int*、char* 等
  2. 智能指针: std::unique_ptr 和 std::shared_ptr

这些支持通过特化 is_meta_pointer_like 类来实现, 要使用这些默认特化, 需包含头文件 pointer.hpp, 此外, 该文件还提供了如何扩展支持自定义指针类型的示例

一旦某类型被元系统识别为指针类型, 可以通过 meta_any 对象解引用这些类型

#include <entt/entt.hpp>
#include <iostream>
 
int main() {
    int value = 42;
    // 将 int* 封装进 meta_any
    entt::meta_any any{&value};
 
    // 检查是否是指针类型
    if(any.type().is_pointer_like()) {
        // 解引用 meta_any,获取指针指向的值
        if(entt::meta_any ref = *any; ref) {
            // 修改被指向的对象
            ref.cast<int>() = 100;
            std::cout << "Value after modification: " << value << "\n"; // 输出 100
        }
    }
 
    return 0;
}

同样, 可以避免多次检查, 解引用失效将返回无效对象

#include <entt/entt.hpp>
#include <iostream>
 
int main() {
    int value = 42;
    entt::meta_any any{&value}; // 将 int* 封装为 meta_any
 
    // 示例 1:重复检查(不必要的方式)
    if (any.type().is_pointer_like()) { // 检查是否是指针类型
        if (entt::meta_any ref = *any; ref) { // 再次检查解引用返回的对象是否有效
            std::cout << "Value: " << ref.cast<int>() << "\n";
        }
    }
 
    // 示例 2:仅检查解引用结果的有效性(推荐方式)
    if (entt::meta_any ref = *any; ref) {
        std::cout << "Value: " << ref.cast<int>() << "\n";
    }
 
    // 示例 3:错误类型,返回无效对象
    entt::meta_any nonPointer{value}; // 封装非指针类型
    if (entt::meta_any invalidRef = *nonPointer; !invalidRef) {
        std::cout << "Failed to dereference: Not a pointer-like type.\n";
    }
 
    return 0;
}

扩展支持自定义指针类型

  1. 类型提供 operator* 或类似机制
    对于支持 operator* 的自定义指针类型, 元系统可以直接工作, 无需额外操作

  2. 提供 ADL 解引用函数
    如果自定义指针类型没有 operator*, 可以通过 ADL 参数依赖查找 机制支持解引用

    template<typename Type>
    Type &dereference_meta_pointer_like(const   custom_pointer_type<Type> &ptr) {
        return ptr.deref();
    }

该方法需要

  • 在指针类型所在的命名空间中定义 dereference_meta_pointer_like
  • deref 是用户定义的解引用方法
  1. 注入到 entt 命名空间
    如果无法修改自定义指针类型的命名空间, 可以通过特化 entt::adl_meta_pointer_like 来支持
    template<typename Type>
    struct  entt::adl_meta_pointer_like<custom_pointer_type<Type>    > {
        static decltype(auto) dereference(const     custom_pointer_type<Type> &ptr) {
            return ptr.deref();
        }
    };

这种方法适用于无法通过 ADL 查找解引用函数的情况, adl_meta_pointer_like 提供了一个更灵活的扩展方式

模版信息 Template information

EnTT 的 元系统 为 模板类 提供了一个最小化的信息集合, 以便用户能够在运行时查询与模板相关的元信息, 使用模板元信息时, 需包含 template.hpp 头文件

默认模版支持

  1. 模版特化检测
    使用 is_template_specialization() 检查一个类型是否是模板类的特化

  2. 获取模板类类型
    template_type() 返回模板类的元类型 meta_type, 通过 entt::meta_class_template_tag 封装

  3. 获取模板参数个数
    template_arity() 返回模板参数的数量

  4. 获取特定模板参数的类型
    template_arg(index) 返回模板参数的元类

#include <entt/entt.hpp>
#include <memory>
#include <iostream>
 
struct my_type {};
 
int main() {
    auto type = entt::resolve<std::shared_ptr<my_type>>();
    
    if(type.is_template_specialization()) {
        std::cout << "This is a template specialization.\n";
 
        // 获取模板类的元类型
        auto class_type = type.template_type();
        std::cout << "Template class: " << class_type.name() << "\n";
 
        // 获取模板参数的数量
        std::size_t arity = type.template_arity();
        std::cout << "Number of template arguments: " << arity << "\n";
 
        // 获取第一个模板参数的元类型
        auto arg_type = type.template_arg(0u);
        std::cout << "First template argument: " << arg_type.name() << "\n";
    }
 
    return 0;
}

假设类型为 std::shared_ptr<my_type>, 则输出为

This is a template specialization.
Template class: std::shared_ptr
Number of template arguments: 1
First template argument: my_type

自定义模版信息

对于某些特殊模板类, 默认提供的信息可能不够详细或不符合需求

template<typename>
struct function_type;
 
template<typename Ret, typename... Args>
struct function_type<Ret(Args...)> {};

对于这种包装函数类型的模板类, 可能需要额外的信息, 比如返回类型和参数类型

扩展元信息

可以通过特化 entt::meta_template_traits 提供自定义的模板信息

template<typename Ret, typename... Args>
struct entt::meta_template_traits<function_type<Ret(Args...)>> {
    using class_type = meta_class_template_tag<function_type>; // 模板类
    using args_type = type_list<Ret, Args...>; // 模板参数(返回类型 + 参数类型)
};
  • class_type: 指定模板类类型, 使用 meta_class_template_tag 封装
  • args_type: 通过 type_list 提供参数类型列表

元系统不会验证 meta_template_traits 中提供的信息是否准确, 所有数据按原样使用, 用户需确保信息的正确性

自动类型转换 Automatic conversions

元系统提供了一种机制, 使自动类型转换成为可能, 从而大大简化了对 算术类型 和 枚举类型 的操作

算术类型的自动转换

C++ 中, 算术类型 (如 int、float、double) 之间存在隐式转换规则, 这些规则允许开发者在编译时自动进行类型转换, 而无需显式指定

int a = 42;
double b = a; // 自动从 int 转换为 double

如果在反射系统中需要显式注册这些转换规则, 则会变得繁琐且容易出错

entt::meta_factory<int>{}
    .conv<bool>()
    .conv<char>()
    .conv<double>(); // 针对每种可能的类型都需要注册

EnTT 支持这些转换的自动化处理, 无需显式注册

#include <entt/entt.hpp>
#include <iostream>
 
int main() {
    entt::meta_any any{42};
 
    // 自动允许转换
    if(any.allow_cast<double>()) {
        double value = any.cast<double>();
        std::cout << "Converted value: " << value << "\n"; // 输出 42.0
    }
 
    return 0;
}

在这里, allow_cast<double>() 检查是否可以转换为目标类型 double, cast<double>() 执行实际的类型转换

枚举类型的自动转换

无作用域枚举 Unscoped Enum

C++ 允许将无作用域枚举隐式转换为其底层类型

enum my_enum { value_a = 1, value_b = 2 };
int x = value_a; // 隐式转换为 int

在 EnTT 中, 无需显式注册这种转换即可实现

enum my_enum { value_a = 1, value_b = 2 };
 
int main() {
    entt::meta_any any{value_a};
 
    if(any.allow_cast<int>()) {
        int value = any.cast<int>();
        std::cout << "Converted enum to int: " << value << "\n"; // 输出 1
    }
 
    return 0;
}

有作用域枚举 Scoped Enum

对于有作用域枚举, C++ 不允许直接隐式转换为底层类型, 但可以通过 std::underlying_type 提供的工具实现

enum class scoped_enum : int { value_a = 1, value_b = 2 };
 
int main() {
    scoped_enum value = scoped_enum::value_a;
    entt::meta_any any{value};
 
    if(any.allow_cast<std::underlying_type_t<scoped_enum>>()) {
        int int_value = any.cast<std::underlying_type_t<scoped_enum>>();
        std::cout << "Converted scoped enum to int: " << int_value << "\n"; // 输出 1
    }
 
    return 0;
}

通过 std::underlying_type_t, 你可以将有作用域枚举显式转换为其底层类型, 且 EnTT 自动支持这种转换

手动注册转换函数

虽然 EnTT 支持自动转换, 但仍可以手动注册转换函数, 以覆盖默认行为或支持特定的复杂转换

#include <entt/entt.hpp>
#include <iostream>
 
struct my_class {
    int value;
};
 
int main() {
    entt::meta<my_class>()
        .type("my_class"_hs)
        .conv<int>([](const my_class &instance) {
            return instance.value;
        });
 
    my_class obj{42};
    entt::meta_any any{obj};
 
    if(any.allow_cast<int>()) {
        int value = any.cast<int>();
        std::cout << "Custom conversion: " << value << "\n"; // 输出 42
    }
 
    return 0;
}

在这个例子中, 为 my_class 注册了一个自定义的转换函数, 将对象转换为其成员变量 value, 当调用 cast<int>() 时, 会执行注册的转换逻辑

自动转换的优先级

  1. 手动注册优先: 如果手动注册了转换函数, EnTT 会优先使用用户提供的实现
  2. 自动转换次之: 在没有手动注册的情况下, EnTT 会尝试执行内置的自动转换

隐式生成默认构造函数 Implicitly generated default constructor

EnTT 的反射系统支持隐式生成默认构造函数, 这意味着对于 默认可构造类型 default-constructible types, 可以直接通过元类型来构造对象, 而无需显式注册元类型或默认构造函数

默认构造的自动支持

隐式支持的类型

  1. 内置类型: 如 int、char 等原始类型
  2. 用户定义的默认可构造类型: 无论是显式定义的默认构造函数还是编译器隐式生成的, 都被支持

通过 meta_type::construct() 方法, 可以直接从元类型构造一个对象

#include <entt/entt.hpp>
#include <iostream>
 
int main() {
    // 通过元类型构造一个整数
    entt::meta_any instance = entt::resolve<int>().construct();
    std::cout << "Constructed int: " << instance.cast<int>() << "\n"; // 输出 0
 
    return 0;
}

这里 resolve<int>() 获取类型 int 的元类型, construct() 调用默认构造函数, 生成一个新的 int 实例 (值为 0)

用户定义的类型

对于用户定义类型, 只要该类型是默认可构造的, 也可以通过元系统隐式构造

#include <entt/entt.hpp>
#include <iostream>
 
struct my_type {
    my_type() : value(42) {} // 默认构造函数
    int value;
};
 
int main() {
    // 注册用户类型
    entt::meta<my_type>().type("my_type"_hs);
 
    // 通过元类型构造实例
    entt::meta_any instance = entt::resolve<my_type>().construct();
    auto &obj = instance.cast<my_type>();
    std::cout << "Constructed my_type with value: " << obj.value << "\n"; // 输出 42
 
    return 0;
}

my_type 的元类型在运行时被解析, 使用 construct() 调用默认构造函数, 成功生成 my_type 的实例

默认构造函数的优先级

如果用户显式注册了默认构造函数, 那么注册的版本会优先于隐式生成的版本

#include <entt/entt.hpp>
#include <iostream>
 
struct my_type {
    int value;
 
    // 默认构造函数
    my_type() : value(0) {}
 
    // 自定义构造函数
    static my_type create() {
        return my_type{100};
    }
};
 
int main() {
    // 注册类型及其自定义构造函数
    entt::meta<my_type>()
        .type("my_type"_hs)
        .ctor<&my_type::create>();
 
    // 通过元类型构造实例
    entt::meta_any instance = entt::resolve<my_type>().construct();
    auto &obj = instance.cast<my_type>();
    std::cout << "Constructed my_type with value: " << obj.value << "\n"; // 输出 100
 
    return 0;
}

当用户注册了 my_type::create 作为构造函数, 元系统会优先调用该函数, 隐式生成的默认构造函数会被覆盖

如果类型没有默认构造函数, 调用 construct() 会失败

struct no_default {
    no_default(int) {}
};
 
// 调用 construct() 会抛出异常
entt::resolve<no_default>().construct(); // 编译或运行时失败

从 void 到 any

EnTT 的 meta_type::from_void 提供了一种将 不透明指针 opaque pointer 转换为 meta_any 的功能, 这对动态类型管理和运行时操作非常有用

当你有一个指向已知元类型对象的 不透明指针 void*时, 可以通过 meta_type::from_void 将其封装为 meta_any

entt::meta_any any = entt::resolve(id).from_void(pointer);

id 是目标类型的 哈希标识符, pointer 是指向对象的 void* 指针

这种转换类似于 C++ 的 static_cast, 比较高效, 允许直接操作已知类型的对象, 但是 没有类型检查, 错误的类型转换会导致未定义行为

#include <entt/entt.hpp>
#include <iostream>
 
struct my_type {
    int value;
};
 
int main() {
    // 注册类型
    entt::meta<my_type>().type("my_type"_hs);
 
    my_type obj{42};          // 原始对象
    void* pointer = &obj;     // 不透明指针
 
    // 从不透明指针构造 meta_any
    entt::meta_any any = entt::resolve("my_type"_hs).from_void(pointer);
 
    // 操作 meta_any
    if(any) {
        auto& ref = any.cast<my_type>();
        std::cout << "Value: " << ref.value << "\n"; // 输出 42
 
        // 修改对象值
        ref.value = 100;
        std::cout << "Updated value: " << ref.value << "\n"; // 输出 100
    }
 
    return 0;
}

输出

Value: 42
Updated value: 100

策略 Policy

在 EnTT 中, Policy 策略 是一种编译时指令, 用于在注册反射信息时自定义行为了, 通过策略, 开发者可以灵活地调整默认行为, 比如避免不必要的拷贝、提供引用访问、或者忽略返回值等

默认行为 (as-is 策略)

策略类型是 entt::as_is_t, 是默认的策略, 如果没有显式指定策略, 系统会自动选择该策略, 使用 as-is 策略时, 函数返回值或数据成员的值会被 复制 到 meta_any 中, 这适用于小型或可高效拷贝的类型, 比如 int、float 等

struct my_type {
    int value;
};
 
entt::meta<my_type>()
    .data<&my_type::value>("value"_hs); // 默认策略为 as_is

通过反射访问 value 时, 会返回该值的一个拷贝

忽略返回值 (as-void 策略)

策略类型是 entt::as_void_t, 使用 as-void 策略会丢弃返回值, 使其看起来像返回 void, 该策略适用于函数、构造函数和数据成员

  • 函数: 忽略返回值
  • 构造函数: 调用构造函数但不返回对象
  • 数据成员: 数据成员变为只写 (无法读取)

适用于函数返回值无关紧要或仅关注副作用和性能优化

struct my_type {
    void action() { /* 执行某些操作 */ }
};
 
entt::meta<my_type>()
    .func<&my_type::action, entt::as_void_t>("action"_hs); // 返回值被忽略

通过反射调用 action 时, 只会执行函数逻辑, 返回值为空

引用访问 (as-ref 和 as-cref 策略)

这个策略可以避免对大型对象的拷贝以及直接操作原始实例

策略类型

  • entt::as_ref_t: 返回对原对象的可变引用, 根据上下文提供可变或只读访问

  • entt::as_cref_t: 返回对原对象的常量引用, 强制返回常量引用, 确保对象不可修改

struct my_type {
    std::string data;
};
 
entt::meta<my_type>()
    .data<&my_type::data, entt::as_ref_t>("data"_hs); // 返回对原对象的引用

通过反射访问 data 时, 可以直接修改原始实例的值, 而不需要额外的拷贝

命名常量和枚举 Named constants and enums

EnTT 的元系统支持将 命名常量 (包括 枚举值 和其他 常量) 反射为类型的 常量数据成员, 这样, 用户可以像操作类成员一样操作枚举值或常量, 简化了 常量 和 枚举 的使用方式

命名常量: 使用 data 函数将任意 常量值 与 元类型 关联, 例如,反射一个 整数常量

枚举: 枚举值可以通过 data 函数暴露为枚举类型的 常量数据成员

特点

  • 所有反射的常量值被视为不可变 const
  • 反射的常量值在元系统中与其对应的元类型绑定
  • 由于 meta_any 使用了 小对象优化 SOO, 访问和操作这些常量时不会产生额外的分配

使用

枚举值可以通过 data 函数注册到枚举的元类型中

#include <entt/entt.hpp>
#include <iostream>
 
enum class my_enum {
    a_value,
    another_value
};
 
int main() {
    // 注册枚举值为元类型的常量
    entt::meta<my_enum>()
        .data<my_enum::a_value>("a_value"_hs)
        .data<my_enum::another_value>("another_value"_hs);
 
    // 获取枚举值
    auto value = entt::resolve<my_enum>()
                     .data("a_value"_hs)
                     .get({})
                     .cast<my_enum>();
 
    std::cout << "Resolved enum value: " << static_cast<int>(value) << "\n"; // 输出 0
 
    return 0;
}

可以为算术类型添加具有特殊意义的常量值

#include <entt/entt.hpp>
#include <iostream>
 
int main() {
    // 为 int 类型注册一个命名常量
    entt::meta<int>().data<2048>("max_int"_hs);
 
    // 获取常量值
    auto max = entt::resolve<int>()
                   .data("max_int"_hs)
                   .get({})
                   .cast<int>();
 
    std::cout << "Resolved constant: " << max << "\n"; // 输出 2048
 
    return 0;
}

统一访问方式

不论是枚举值还是普通常量, 都可以通过以下步骤访问:

  1. 使用 resolve<type>() 获取元类型
  2. 调用 data("name"_hs) 获取对应常量的元数据
  3. 使用 get({}) 获取实际值
  4. 调用 cast<type>() 转换为目标类型

应用场景

枚举值的动态解析

在运行时解析枚举值

std::string input = "a_value";
auto enum_data = entt::resolve<my_enum>().data(entt::hashed_string::value(input.c_str()));
if(enum_data) {
    auto value = enum_data.get({}).cast<my_enum>();
    std::cout << "Parsed enum value: " << static_cast<int>(value) << "\n";
}

这对于需要根据字符串动态获取枚举值的场景非常有用, 比如配置文件解析或命令行处理

常量的元数据增强

为基础类型 (如 int) 定义特殊常量值, 可以通过元系统直接获取它们

entt::meta<int>().data<1024>("default_buffer_size"_hs);
auto buffer_size = entt::resolve<int>().data("default_buffer_size"_hs).get({}).cast<int>();
std::cout << "Default buffer size: " << buffer_size << "\n"; // 输出 1024

这种方式使常量更具描述性, 并便于动态查询

小对象优化

由于 meta_any 实现了 小对象优化, 小型数据 (如枚举值或小整数) 在堆栈上直接存储, 而无需动态分配内存, 这种优化显著提高了性能, 特别是在频繁操作常量时

注意事项

  1. 只读特性: 注册为常量的数据无法通过反射修改, 因为其被视为只读属性

  2. 哈希字符串: 在注册常量时需要使用哈希字符串 (如 "a_value"_hs), 这种方式高效且安全, 但用户需要确保字符串唯一性

  3. 类型绑定: 常量值始终与其注册的元类型绑定, 例如, 反射的 int 常量无法直接用作 double, 需要显式转换

用户定义数据 User defined data

EnTT 提供了在 元对象 中附加 用户自定义数据 或 特性 traits (用于存储简单的标志位) 的功能, 这使得开发者可以为反射系统扩展额外的信息, 无论是 创建编辑器 还是 元数据管理, 附加这些数据都非常有用

自定义特性 Traits

用户定义的特性是简单的标志位或整数值, 存储在一个 16 位的位掩码中, 访问性能非常高, 由于最多支持 16 位, 用户可以使用 16 个 标志位 bitmask 或 2^16 个整数值

设置特性

特性通过 traits 函数设置

enum my_traits : std::uint16_t {
    required = 1 << 0,
    hidden = 1 << 1,
    internal = 1 << 2
};
 
entt::meta_factory<my_type>{}.traits(my_traits::required | my_traits::hidden);

调用 traits 时会覆盖之前设置的特性, 如果需要扩展现有特性, 必须通过工厂重新更新, 每次调用 traits 时需要传入完整的位掩码

访问特性

设置特性后, 可以通过元对象的 traits 函数读取特性值

auto value = entt::resolve<my_type>().traits<my_traits>();
if(value & my_traits::hidden) {
    std::cout << "This type is hidden.\n";
}

扩展特性

可以在创建元对象后通过工厂重置目标元对象并添加新的特性

entt::meta_factory<my_type>{}
    .data<&my_type::data_member>("member"_hs)
    .traits(my_traits::internal); // 添加新的特性

自定义数据 Custom Data

自定义数据可以是任何类型, 存储在库为用户保留的区域中, 常用于为元类型、数据成员或函数附加额外信息, 例如描述文本或元信息

设置自定义数据

通过 custom 函数可以附加自定义数据

struct type_data {
    std::string description;
};
 
entt::meta_factory<my_type>{}.custom<type_data>({"This is a custom type"});

调用 custom 时会覆盖之前的自定义数据, 如果需要更新现有数据, 可以通过工厂重置目标元对象

访问自定义数据

设置自定义数据后, 可以通过 custom 函数读取

const type_data &value = entt::resolve<my_type>().custom<type_data>();
std::cout << "Description: " << value.description << "\n";

如果读取失败 (例如类型不匹配), 在非调试模式下会抛出异常或返回空指针 (取决于调用方式)

附加自定义数据到成员或函数

除了类型本身, 自定义数据也可以附加到数据成员或函数

struct function_data {
    std::string tooltip;
};
 
entt::meta_factory<my_type>{}
    .func<&my_type::member_function>("member"_hs)
    .custom<function_data>({"This is a tooltip for the function"});

读取附加到函数的数据:

const function_data &value = entt::resolve<my_type>()
                                 .func("member"_hs)
                                 ->custom<function_data>();
std::cout << "Tooltip: " << value.tooltip << "\n";

应用场景

  1. 编辑器开发
    在编辑器中为类型、数据成员和函数附加描述信息或标志位:

    • 特性可用于标识是否在编辑器中显示某些类型或成员
    • 自定义数据可存储描述信息、提示文本等
  2. 运行时扩展
    在运行时动态读取附加的特性或数据, 为类型提供更多功能:

    • 使用特性区分类型的行为
    • 使用自定义数据扩展类型的元信息
  3. 调试工具
    附加调试信息, 例如函数调用的用途或限制说明

注销类型

在 EnTT 的反射系统中, 可以对已注册的类型进行 注销 Unregister, 注销类型会删除所有与该类型关联的元对象, 包括其数据成员、成员函数、转换函数等, 这一功能允许在运行时动态调整反射系统的内容, 也便于释放不再需要的元数据

可以使用模板形式的 meta_reset 函数注销某个特定类型

entt::meta_reset<my_type>();

断开该类型与所有元对象的关联, 删除该类型的唯一标识符, 使其无法再通过反射系统访问, 该操作不会影响基类, 因为基类可能不依赖于此类型

可以通过类型的 唯一标识符 (哈希字符串) 注销类型, 而无需直接使用类型本身

entt::meta_reset("my_type"_hs);

适用于在运行时只知道类型的标识符, 而非模板类型本身, 通过动态标识符管理多个类型的元数据

可以调用无参版本的 meta_reset 函数, 注销所有已注册的元类型

entt::meta_reset();

清空反射系统中的所有类型及其关联的元数据, 适合需要彻底重置反射系统的场景

元上下文 Meta context

在 EnTT 的反射系统中, Meta Context 元上下文 是一个运行时管理的对象, 用于存储所有元类型及其相关数据, 默认情况下, 所有元类型都会被注册到一个全局的默认上下文中, 但用户可以替换默认上下文或者创建多个自定义上下文, 以便在不同场景中灵活管理元类型

默认上下文

默认上下文通过 服务定位器 Service Locator 访问

auto &&context = entt::locator<entt::meta_context>::value_or();

默认上下文的特点

  • 单一实例: 默认上下文在应用程序中是全局的
  • 自动管理: 元类型默认注册到这个上下文中

用户可以随时替换默认上下文, 例如在测试环境中使用一个新的上下文

entt::meta_context other{}; // 创建新的上下文
auto &&context = entt::locator<entt::meta_context>::value_or();
std::swap(context, other);  // 替换默认上下文

比如在单元测试中, 替换默认上下文以隔离测试环境, 替换默认上下文后, 可以清空先前的元类型定义, 避免干扰

自定义上下文

如果需要多个上下文来管理不同的元类型集合, 可以创建独立的上下文, 并在需要时使用

entt::meta_ctx context{}; // 创建自定义上下文
entt::meta_factory<my_type>{context}.type("reflected_type"_hs); // 在自定义上下文中注册类型
 
// 构造 meta_any
entt::meta_any any{context, std::in_place_type<my_type>};
 
// 解析类型
entt::meta_type type = entt::resolve(context, "reflected_type"_hs);

自定义上下文中的元类型对默认上下文不可见, 必须显式传递上下文才能访问相关的元数据

多上下文使用

隔离不同的模块

在模块化应用程序中, 可以为每个模块创建一个独立的上下文, 从而避免模块之间的元类型冲突

entt::meta_ctx graphics_context{};
entt::meta_ctx physics_context{};
 
// 在图形模块中注册类型
entt::meta_factory<graphics_type>{graphics_context}.type("graphics_type"_hs);
 
// 在物理模块中注册类型
entt::meta_factory<physics_type>{physics_context}.type("physics_type"_hs);

动态切换上下文

在运行时根据需求切换上下文

entt::meta_ctx context1{};
entt::meta_ctx context2{};
 
// 根据条件选择上下文
entt::meta_ctx &active_context = (use_context1 ? context1 : context2);
 
// 使用选定的上下文解析类型
auto type = entt::resolve(active_context, "type_name"_hs);

实例

#include <entt/entt.hpp>
#include <iostream>
 
struct graphics_type {
    int width, height;
};
 
struct physics_type {
    float mass, velocity;
};
 
int main() {
    // 创建两个独立的上下文
    entt::meta_ctx graphics_context{};
    entt::meta_ctx physics_context{};
 
    // 在图形上下文中注册类型
    entt::meta_factory<graphics_type>{graphics_context}
        .type("graphics_type"_hs)
        .data<&graphics_type::width>("width"_hs)
        .data<&graphics_type::height>("height"_hs);
 
    // 在物理上下文中注册类型
    entt::meta_factory<physics_type>{physics_context}
        .type("physics_type"_hs)
        .data<&physics_type::mass>("mass"_hs)
        .data<&physics_type::velocity>("velocity"_hs);
 
    // 使用图形上下文解析类型
    auto graphics_type = entt::resolve(graphics_context, "graphics_type"_hs);
    if (graphics_type) {
        std::cout << "graphics_type found in graphics_context.\n";
    }
 
    // 使用物理上下文解析类型
    auto physics_type = entt::resolve(physics_context, "physics_type"_hs);
    if (physics_type) {
        std::cout << "physics_type found in physics_context.\n";
    }
 
    // 尝试在错误的上下文中解析类型
    if (!entt::resolve(graphics_context, "physics_type"_hs)) {
        std::cout << "physics_type not found in graphics_context.\n";
    }
 
    return 0;
}

输出

graphics_type found in graphics_context.
physics_type found in physics_context.
physics_type not found in graphics_context.