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" 是人类可读的字符串
  • _hshashed_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), 运行时可通过名字访问

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

  • 自由函数: 独立于类的函数
  • 类成员函数: 属于类的成员函数
  • 二者的混合: gettersetter 分别使用自由函数或成员函数的组合
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_anyentt::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, 可以用于查询数据成员是否为 conststatic, 或者获取并设置数据成员的值

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, 可以查询函数是否为 conststatic, 或者获取函数的参数个数、返回值类型和参数类型

// 调用函数
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::vectorstd::arraystd::dequestd::list (不包括 std::forward_list)
  • 关联容器: std::mapstd::set 及其无序版本 (std::unordered_mapstd::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. beginend
    返回容器的迭代器, 可以用于直接遍历容器

    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. beginend
    返回容器的迭代器, 可以用于直接遍历容器:

    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_ptrstd::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++ 中, 算术类型 (如 intfloatdouble) 之间存在隐式转换规则, 这些规则允许开发者在编译时自动进行类型转换, 而无需显式指定

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. 内置类型: 如 intchar 等原始类型
  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 中, 这适用于小型或可高效拷贝的类型, 比如 intfloat

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标志位 bitmask2^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.