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: 尝试将存储的对象转换为目标类型, 如果失败返回nullptrcast: 强制转换存储的对象为目标类型 (抛出异常或断言失败时)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 的元系统识别, 需要为以下类之一进行特化
meta_sequence_container_traits: 用于序列容器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 的代理对象接口
-
value_type
返回容器元素的元类型meta_type -
size
返回容器中元素的数量, 类型为无符号整数 -
resize
修改容器大小并返回true表示成功, 对于固定大小的容器 (如std::array), 此操作将失败 -
clear
清空容器并返回true表示成功, 固定大小的容器无法执行此操作 -
reserve
预分配容器容量,返回 true 表示成功, 固定大小的容器无法执行此操作 -
begin和end
返回容器的迭代器, 可以用于直接遍历容器
for(entt::meta_any element : view) {
// 处理元素
}insert
在指定位置插入元素
auto last = view.end();
view.insert(last, 42); // 在末尾插入整数 42返回插入位置的迭代器以及一个布尔值, 表示操作是否成功, 固定大小容器或类型转换失败时会返回无效的结果
erase
删除指定位置的元素
auto first = view.begin();
view.erase(first); // 删除第一个元素返回指向下一个元素的迭代器, 以及一个布尔值, 表示操作是否成功
operator[]
按位置访问容器中的元素
for(std::size_t pos = 0; pos < view.size(); ++pos) {
entt::meta_any value = view[pos];
// 处理元素
}返回的是一个直接引用容器中实际元素的 meta_any, 修改返回的对象会直接修改容器中的元素, 注意, 对于如 std::list 这样的容器, 位置访问效率较低, 因为需要线性遍历
关联容器 Associative Containers 的代理对象接口
-
key_only
如果容器是只包含键的 (如std::set), 返回true -
key_type
返回键的元类型meta_type -
mapped_type
- 对于只包含键的容器 (如
std::set), 返回无效的meta_type - 对于键值对容器 (如
std::map), 返回值的 元类型
value_type
返回容器元素的元类型
- 对于
std::set<int>, 返回int - 对于
std::map<int, char>, 返回std::pair<const int, char>
-
size
返回容器中元素的数量 -
clear
清空容器, 返回true表示成功 -
reserve
预分配容量, 返回true表示成功 (不适用于标准映射类型) -
begin和end
返回容器的迭代器, 可以用于直接遍历容器:
for(std::pair<entt::meta_any, entt::meta_any> element : view) {
// element.first 是键
// element.second 是值
}对于只包含键的容器, element.second 是一个无效的 meta_any
insert
插入键值对
view.insert(view.end().handle(), 42, 'c'); // 插入键 42 和值 'c'返回一个布尔值, 表示插入是否成功, 类型转换失败时插入操作会失败
erase
根据键删除元素
view.erase(42); // 删除键为 42 的元素返回布尔值,表示删除是否成功
operator[]
按键访问值
entt::meta_any value = view[42];返回一个直接引用容器中实际元素的 meta_any, 修改返回的对象会直接修改容器中的元素
类指针类型 Pointer-like types
EnTT 的元系统支持指针类型, 这使得用户可以通过 meta_any 解引用指针类型的实例, 从而获取与其元类型正确关联的轻量级引用, 这种设计允许用户直接修改被指针指向的对象, 或者通过扩展支持自定义指针类型
默认支持以下指针类型:
- 原生指针 raw pointers: 如
int*、char*等 - 智能指针:
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;
}扩展支持自定义指针类型
-
类型提供
operator*或类似机制
对于支持operator*的自定义指针类型, 元系统可以直接工作, 无需额外操作 -
提供 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是用户定义的解引用方法
- 注入到
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 头文件
默认模版支持
-
模版特化检测
使用is_template_specialization()检查一个类型是否是模板类的特化 -
获取模板类类型
template_type()返回模板类的元类型meta_type, 通过entt::meta_class_template_tag封装 -
获取模板参数个数
template_arity()返回模板参数的数量 -
获取特定模板参数的类型
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>() 时, 会执行注册的转换逻辑
自动转换的优先级
- 手动注册优先: 如果手动注册了转换函数, EnTT 会优先使用用户提供的实现
- 自动转换次之: 在没有手动注册的情况下, EnTT 会尝试执行内置的自动转换
隐式生成默认构造函数 Implicitly generated default constructor
EnTT 的反射系统支持隐式生成默认构造函数, 这意味着对于 默认可构造类型 default-constructible types, 可以直接通过元类型来构造对象, 而无需显式注册元类型或默认构造函数
默认构造的自动支持
隐式支持的类型
- 内置类型: 如
int、char等原始类型 - 用户定义的默认可构造类型: 无论是显式定义的默认构造函数还是编译器隐式生成的, 都被支持
通过 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;
}统一访问方式
不论是枚举值还是普通常量, 都可以通过以下步骤访问:
- 使用
resolve<type>()获取元类型 - 调用
data("name"_hs)获取对应常量的元数据 - 使用
get({})获取实际值 - 调用
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 实现了 小对象优化, 小型数据 (如枚举值或小整数) 在堆栈上直接存储, 而无需动态分配内存, 这种优化显著提高了性能, 特别是在频繁操作常量时
注意事项
-
只读特性: 注册为常量的数据无法通过反射修改, 因为其被视为只读属性
-
哈希字符串: 在注册常量时需要使用哈希字符串 (如
"a_value"_hs), 这种方式高效且安全, 但用户需要确保字符串唯一性 -
类型绑定: 常量值始终与其注册的元类型绑定, 例如, 反射的
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";应用场景
-
编辑器开发
在编辑器中为类型、数据成员和函数附加描述信息或标志位:- 特性可用于标识是否在编辑器中显示某些类型或成员
- 自定义数据可存储描述信息、提示文本等
-
运行时扩展
在运行时动态读取附加的特性或数据, 为类型提供更多功能:- 使用特性区分类型的行为
- 使用自定义数据扩展类型的元信息
-
调试工具
附加调试信息, 例如函数调用的用途或限制说明
注销类型
在 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.