Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

运行时反射系统

目录

简介

反射(或更确切地说,它的缺失)是 C++ 领域的热门话题,在 EnTT 的特定场景中,它是一个能解锁许多有趣功能的工具。我曾寻找满足我需求的第三方库,但总是遇到一些我不喜欢的细节:宏、侵入性、过多的内存分配等等。
最终,我决定为 EnTT 编写一个内置的、非侵入式且无宏的运行时反射系统。也许我做得并不比别人好,也许是的,时间会证明一切,但至少我可以围绕它所属的库来定制这个工具,而不是让库去适应工具。

标识符

在处理标识符时,meta 系统不强制用户依赖库提供的工具。它通过提供一套适用于整数值的 API 来实现,这些整数值可以是通过 hashed string 生成的,也可以不是。
这意味着用户可以为 meta 对象分配任何类型的标识符,只要它们是数值。它们是在运行时、编译时生成的,还是通过自定义函数生成的,都无关紧要。

话虽如此,以下部分的一些示例基于本库提供的 hashed_string 类。因此,在提供整数标识符的地方,很可能会使用如下的用户定义字面量:

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

就其本身而言,这完全等同于:

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

显然,人类可读的标识符使用起来更方便,也强烈推荐。

反射简述

反射始终从实际的 C++ 类型开始。用户不能反射 虚构的 类型。
meta_factory 类是一切的起点:

entt::meta_factory<my_type> factory{};

返回的值是一个 factory 对象,用于继续构建 meta 类型。

默认情况下,meta 类型与 EnTT 内置的运行时类型识别系统返回的标识符相关联。
但是,也可以为 meta 类型分配自定义标识符:

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

如果需要,在运行时 检索 meta 类型时使用标识符而不是类型。
然而,用户可能只对向反射类型添加功能感兴趣,以便反射系统可以在底层正确使用它,而不希望使该类型变得 可搜索。在这种情况下,只需不调用 type 即可。

Factory 的所有成员函数都返回 factory 本身。它通常用于创建以下内容:

  • 构造函数。通过指定 参数列表 将构造函数分配给反射类型。如果返回类型符合预期,也接受自由函数。从客户端的角度来看,自由函数或实际构造函数之间没有任何区别:

    entt::meta_factory<my_type>{}.ctor<int, char>().ctor<&factory>();
    

    如果可能,会隐式生成 meta 默认构造函数。

  • 数据成员。Meta 数据成员是底层类型的实际数据成员,也可以是任何类型的静态和全局变量或常量。从客户端的角度来看,与反射类型关联的所有变量看起来都像是类型本身的一部分:

    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 函数需要用于 meta 数据成员的标识符。然后将其用于运行时访问。
    数据成员也可以通过 setter 和 getter 对来定义。这些可以是自由函数、类成员或它们的混合。这种方法对于从非 const 数据成员创建只读属性也很方便:

    entt::meta_factory<my_type>{}.data<nullptr, &my_type::data_member>("member"_hs);
    
  • 成员函数。Meta 成员函数是底层类型的实际成员函数,也可以是普通的自由函数。从客户端的角度来看,与反射类型关联的所有函数看起来都像是类型本身的一部分:

    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 函数需要用于 meta 函数的标识符。然后将其用于运行时访问。
    支持 meta 函数重载。反射系统会在运行时根据参数类型解析重载函数。

  • 基类。基类是指底层类型实际派生自它的类:

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

    反射系统会跟踪这种关系,并在需要时允许在运行时进行隐式转换。换句话说,在任何需要 base_type 的地方,也接受 derived_type 的实例。

  • 转换函数。转换函数允许用户定义反射系统在需要时隐式执行的转换:

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

这就是用户创建 meta 类型所需的全部内容。有关更多详细信息,请参阅内联文档。

Any 来救场

反射系统提供了一种 entt::any 类的 扩展版本(有关更多详细信息,请参阅 core 模块)。
其目的是在现有功能的基础上增加一些特性,以便将其与 meta 类型系统集成,而无需重复代码。

其 API 与 any 类型非常相似。meta_any包装 了许多推断 meta 节点的功能,然后将部分或全部参数转发到底层存储。
在少数显著差异中,meta_any 增加了对容器和类指针类型 (pointer-like types) 的支持,而 any 则没有。
any 类似,此类也用于使用 forward_as_meta 或使用 std::in_place_type<T &> 消歧标签为未托管对象创建 别名,以及通过 as_ref 成员函数从现有对象创建别名。此外,它还可以通过 std::in_place 接管作为参数传递的指针的所有权。
然而,与 any 不同的是,meta_any 对空实例和使用 void 初始化的实例区别对待:

entt::meta_any empty{};
entt::meta_any other{std::in_place_type<void>};

虽然 any 将两者都视为空,但 meta_any 将使用 void 初始化的对象视为 有效 对象。这允许区分失败的函数调用和成功但没有返回值的函数调用。

最后,成员函数 try_castcastallow_cast 用于将底层对象转换为给定类型(引用或值类型),或 转换 meta_any 以使转换对结果对象可行。
事实上,meta_any 没有 any_cast 的等效物。

享受运行时

一旦构建了反射类型的网络,剩下的就是在运行时在需要的地方使用它。
有几种选项可以搜索反射类型:

// 直接访问反射类型
auto by_type = entt::resolve<my_type>();

// 通过标识符查找反射类型
auto by_id = entt::resolve("reflected_type"_hs);

// 通过 type info 查找反射类型
auto by_type_id = entt::resolve(entt::type_id<my_type>());

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

for(auto &&[id, type]: entt::resolve()) {
    // ...
}

在所有情况下,返回的值都是 meta_type 的实例(可能带有其 id)。这些对象提供了一套 API,用于了解它们的 运行时标识符、迭代与它们关联的所有 meta 对象,甚至构建底层类型的实例。
Meta 数据成员和函数通过它们的标识符访问:

  • Meta 数据成员:

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

    返回的类型是 meta_data,如果没有与给定标识符关联的 meta 数据对象,则可能无效。
    meta 数据对象提供了一套 API,用于查询底层类型(例如,了解它是否是 const 或 static)、获取变量的 meta 类型以及设置或获取包含的值。

  • Meta 函数成员:

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

    返回的类型是 meta_func,如果没有与给定标识符关联的 meta 函数对象,则可能无效。
    meta 函数对象提供了一套 API,用于查询底层类型(例如,了解它是否是 const 或 static 函数)、了解参数数量、meta 返回类型和参数的 meta 类型。此外,meta 函数对象用于调用底层函数,然后以 meta_any 对象的形式获取返回值。

这两个函数都在整个 meta 类型层次结构中搜索元素。但是,它们提供了传递第二个布尔参数的选项,以将搜索限制在顶层 meta 类型。
由此获得的所有 meta 对象以及 meta 类型都显式转换为布尔值以检查有效性:

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

此外,它们中的所有对象(以及一些其他对象,如 meta 基类)都由一堆重载返回,这些重载为调用者提供顶层元素的可迭代范围。例如:

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

Meta 类型也用于 construct 底层类型的实际实例。
特别是,construct 成员函数接受可变数量的参数并搜索匹配项。然后它返回一个 meta_any 对象,该对象可能已初始化也可能未初始化,具体取决于是否找到了合适的构造函数。
转换函数则不可访问。它们在需要时由 meta_any 和 meta 对象在内部使用。

Meta 类型和一般的 meta 对象包含的内容远比上述提到的多。有关更多详细信息,请参阅内联文档。

告诉我你的名称

对于 meta 类型、数据和函数,用户还可以提供自定义 名称 (names)

entt::meta_factory<my_type>{}
    .type("type"_hs, "my_type")
    .data<&variable>("data"_hs, "variable")
    .func<&function>("func"_hs, "function");

提供的 标签 (label) 应该 是一个字符串字面量。库不会进行拷贝。由用户保证名称本身的生命周期。
字符串标识符通过 name 函数从 meta 对象返回:

const char *name = entt::resolve<my_type>().name();

由于大多数情况下不需要将 名称 与与 meta 对象关联的数字标识符区分开来,EnTT 还提供了这些函数的更紧凑版本:

entt::meta_factory<my_type>{}
    .type("my_type")
    .data<&variable>("variable")
    .func<&function>("function");

同样,提供的名称 应该 是一个字符串字面量。然后使用该字符串通过 hashed_string 类生成数字标识符。
尽管支持名称,但没有可用的基于字符串的查找函数。也就是说,类型 (resolve) 以及数据成员 (data) 和函数成员 (func) 只能 通过数字标识符 搜索

容器支持

运行时反射系统还支持所有类型的容器。
此外,容器 并不一定意味着 C++ 标准库提供的那些容器。事实上,用户定义的数据结构在许多情况下也可以与 meta 系统协同工作。

容器基于一些常见的 traits 自动 检测
例如,序列容器必须具有返回前向迭代器的 begin/end 对,而关联容器还必须提供 key_type 成员和 find 函数。
如果容器未被识别为容器,仍然可以通过特化模板类 meta_sequence_container_traitsmeta_associative_container_traits 来提供 适配器 (adapter)。同样,用户可以通过特化正确的 traits 类但不提供任何定义来 抑制 将类型检测为 meta 容器。

标准库容器通常开箱即用地导出为 meta 容器(std::string 除外,它故意不被视为序列容器)。
但是,必须包含头文件 container.hpp,以便在需要时向编译器提供正确的特化。
该文件还包含一些示例,供那些有兴趣使自己的容器对 meta 系统可用的用户参考。

对于 meta 容器,meta_any 类返回正确初始化的代理对象 (proxy objects) 以方便使用。以下是用户如何访问序列容器的代理对象的故意冗长的示例:

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) {
        // ...
    }
}

关联容器的代理对象通过调用 as_associative_container 以相同的方式访问。
实际上不需要执行双重检查。相反,查询 meta 类型或验证代理对象是否有效就足够了。事实上,代理对象可以上下文转换为 bool 以检查有效性。例如,当包装的对象不是容器时,会返回无效的代理对象。
在所有情况下,都不希望用户显式 反射 容器。只需将存在 traits 类特化的容器分配给 meta_any 对象,即可获取其代理对象。

meta_sequence_container 代理对象的接口对于所有类型的序列容器都是相同的,尽管可用功能因情况而异。特别是:

  • value_type 成员函数返回元素的 meta 类型。

  • size 成员函数以无符号整数值的形式返回容器中的元素数量。

  • resize 成员函数允许调整包装容器的大小,并在成功时返回 true。
    例如,无法调整固定大小容器的大小。

  • clear 成员函数允许清空包装的容器,并在成功时返回 true。
    例如,无法清空固定大小的容器。

  • reserve 成员函数允许增加包装容器的容量,并在成功时返回 true。
    例如,无法增加固定大小容器的容量。

  • beginend 成员函数返回用于直接迭代容器的不透明迭代器:

    for(entt::meta_any element: view) {
        // ...
    }
    

    在所有情况下,给定类型为 C 的底层容器,返回的元素包含一个类型为 C::value_type 的对象,因此这取决于实际的容器。
    所有 meta 迭代器都是输入迭代器,并且故意不提供间接引用运算符。

  • insert 成员函数用于向容器添加元素。它接受一个 meta 迭代器和要插入的元素:

    auto last = view.end();
    // 向容器追加一个整数
    view.insert(last, 42);
    

    此函数返回一个指向插入元素的 meta 迭代器和一个指示操作是否成功的布尔值。如果是固定大小的容器,或者参数至少不能转换为所需的类型,对 insert 的调用可能会静默失败。
    由于 meta 迭代器可以上下文转换为 bool,用户可以依赖它们来了解操作是在实际容器上失败还是在上游失败(例如由于参数转换问题)。

  • erase 成员函数用于从容器中移除元素。它接受一个指向要移除元素的 meta 迭代器:

    auto first = view.begin();
    // 从容器中移除第一个元素
    view.erase(first);
    

    此函数返回最后一个被移除元素之后的 meta 迭代器和一个指示操作是否成功的布尔值。如果是固定大小的容器,对 erase 的调用可能会静默失败。

  • operator[] 用于访问容器元素。它接受单个参数,即要返回的元素的位置:

    for(std::size_t pos{}, last = view.size(); pos < last; ++pos) {
        entt::meta_any value = view[pos];
        // ...
    }
    

    该函数返回直接引用实际元素的 meta_any 实例。直接修改返回的对象会修改容器内的元素。
    根据底层的序列容器,此操作可能不那么高效。例如,在 std::list 的情况下,位置访问会转化为对列表本身的线性遍历(这可能不是用户所期望的)。

同样,meta_associative_container 代理对象的接口对于所有类型的关联容器也是相同的。然而,在仅键 (key-only) 容器的情况下,行为存在一些差异。特别是:

  • key_only 成员函数如果包装的容器是仅键容器,则返回 true。

  • key_type 成员函数返回键的 meta 类型。

  • mapped_type 成员函数对于仅键容器返回无效的 meta 类型,对于所有其他类型的容器返回映射值的 meta 类型。

  • value_type 成员函数返回元素的 meta 类型。
    例如,对于 std::set<int> 它返回 int 的 meta 类型,而对于 std::map<int, char> 它返回 std::pair<const int, char> 的 meta 类型。

  • size 成员函数以无符号整数值的形式返回容器中的元素数量。

  • clear 成员函数允许清空包装的容器,并在成功时返回 true。

  • reserve 成员函数允许增加包装容器的容量,并在成功时返回 true。
    例如,无法增加标准 map 的容量。

  • beginend 成员函数返回用于直接迭代容器的不透明迭代器:

    for(std::pair<entt::meta_any, entt::meta_any> element: view) {
        // ...
    }
    

    在所有情况下,给定类型为 C 的底层容器,返回的元素是一个键值对,其中键的类型为 C::key_type,值的类型为 C::mapped_type。由于仅键容器没有映射类型,它们的 只不过是一个无效的 meta_any 对象。
    所有 meta 迭代器都是输入迭代器,并且故意不提供间接引用运算符。

    虽然访问的键在关联容器中通常是常量,因此按拷贝返回,但值(如果有的话)由直接引用实际元素的 meta_any 实例包装。直接修改它会修改容器内的元素。

  • insert 成员函数用于向容器添加元素。它获取两个参数,即要插入的键和值:

    auto last = view.end();
    // 向容器追加一个整数
    view.insert(last.handle(), 42, 'c');
    

    此函数返回一个布尔值,指示操作是否成功。如果参数至少不能转换为所需的类型,对 insert 的调用可能会失败。

  • erase 成员函数用于从容器中移除元素。它获取单个参数,即要移除的键:

    view.erase(42);
    

    此函数返回一个布尔值,指示操作是否成功。如果参数至少不能转换为所需的类型,对 erase 的调用可能会失败。

  • operator[] 用于访问容器中的元素。它获取单个参数,即要返回的元素的键:

    entt::meta_any value = view[42];
    

    该函数返回直接引用实际元素的 meta_any 实例。直接修改返回的对象会修改容器内的元素。

容器支持是最小化的,但很可能足以满足所有需求。

类指针类型

与容器一样,也可以 告诉 meta 系统哪些类型是 指针。这使得解引用 meta_any 的实例成为可能,从而获得指向对象的轻量级 引用,这些引用也正确地与它们的 meta 类型相关联。
要使 meta 系统将类型识别为 类指针 (pointer-like),用户可以特化 is_meta_pointer_like 类。EnTT 已经导出了一些常见类的特化。特别是:

  • 所有类型的原始指针。
  • std::unique_ptrstd::shared_ptr
  • 所有 导出 名为 is_meta_pointer_like 的类型成员的类:
    struct smart_pointer {
        using is_meta_pointer_like = void;
        // ...
    };
    
    实际类型无关紧要,不会以任何方式使用。

必须包含头文件 pointer.hpp,以便在需要时向编译器提供这些特化。
该文件还包含许多示例,供那些有兴趣使自己的类指针类型对 meta 系统可用的用户参考。

当类型被 meta 系统识别为类指针类型时,可以解引用包含这些对象的 meta_any 实例。以下是故意冗长的示例,展示如何使用此功能:

int value = 42;
// 等同于 int * 的 meta 类型
entt::meta_any any{&value};

if(any.type().is_pointer_like()) {
    // 等同于 int 的 meta 类型
    if(entt::meta_any ref = *any; ref) {
        // ...
    }
}

不需要执行双重检查。相反,查询 meta 类型或验证返回的对象是否有效就足够了。例如,当包装的对象不是类指针类型时,会返回无效实例。
解引用类指针对象会返回一个 引用 指向对象的 meta_any 实例。修改它意味着直接修改指向的对象(除非返回的元素是 const)。

通常,解引用 类指针类型归结为 *ptr。然而,EnTT 也支持不提供 operator* 的类。特别是:

  • 可以通过实现名为 dereference_meta_pointer_like 的函数(也可以是模板函数)来利用基于 ADL 查找的解决方案:

    template<typename Type>
    Type & dereference_meta_pointer_like(const custom_pointer_type<Type> &ptr) {
        return ptr.deref();
    }
    
  • 当无法控制类型的命名空间时,可以将 adl_meta_pointer_like 类模板的特化注入到 entt 命名空间中,以完全绕过 ADL 查找:

    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();
        }
    };
    

在所有其他情况下,并且当解引用指针无论指向的类型如何都能按预期工作时,不需要用户干预。

模板信息

如果原始类型是类模板,Meta 类型还提供有关其 性质 的最小信息集。
默认情况下,这开箱即用,不需要用户操作。但是,必须包含头文件 template.hpp,以便在需要时向编译器提供此信息。

Meta 模板信息很容易找到:

// 如果类型被识别为类模板特化,此方法返回 true
if(auto type = entt::resolve<std::shared_ptr<my_type>>(); type.is_template_specialization()) {
    // 由 entt::meta_class_template_tag 方便地包装的类模板的 meta 类型
    auto class_type = type.template_type();

    // 模板参数的数量
    std::size_t arity = type.template_arity();

    // 第 i 个参数的 meta 类型
    auto arg_type = type.template_arg(0u);
}

通常,当需要类型的模板信息时,库提供的信息就足够了。然而,在某些情况下,用户可能需要更多详细信息或不同的信息集。
考虑一个旨在包装函数类型的类模板的情况:

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...>;
};

反射系统不验证信息的准确性,也不推断实际类型和 meta 类型之间的对应关系。
因此,特化按原样使用,其中包含的信息在需要时与适当的类型相关联。

自动转换

在 C++ 中,算术类型之间允许进行许多转换,这使得处理此类数据非常方便。
如果将其转换为向反射系统显式注册,将会导致一长串如下所示的指令:

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

对每种有资格进行此类转换的类型重复此操作。这既容易出错又重复。
同样,该语言允许用户将无作用域枚举 (unscoped enums) 静默转换为其底层类型,并提供了对有作用域枚举 (scoped enums) 执行相同操作所需的一切。如果显式完成,将导致以下结果:

entt::meta_factory<my_enum>{}
    .conv<std::underlying_type_t<my_enum>>();

幸运的是,所有这些都可以避免。EnTT 为这些类型的转换提供隐式支持:

entt::meta_any any{42};
any.allow_cast<double>();
double value = any.cast<double>();

无需注册,转换会在底层自动进行。涉及 meta 类型的 allow_cast 调用也是如此:

entt::meta_type type = entt::resolve<int>();
entt::meta_any any{my_enum::a_value};
any.allow_cast(type);
int value = any.cast<int>();

这使得处理算术类型以及有作用域或无作用域枚举就像在 C++ 中一样简单。
仍然可以手动设置转换函数,并且这些函数始终优先于自动转换函数。

隐式生成的默认构造函数

通过反射系统创建默认可构造类型的对象,而无需显式注册 meta 类型或其默认构造函数也是可能的。
例如,对于像 intchar 这样的原始类型,但不限于它们。

仅对于默认可构造类型,默认构造函数会自动定义并与其 meta 类型关联,无论它们是显式还是隐式生成的。
因此,这就是从其 meta 类型构造整数所需的全部:

entt::resolve<int>().construct();

例如,当 meta 类型是从 meta 容器返回的类型时,这对于在不知道或不必注册实际类型的情况下构建键很有用。

在所有情况下,当用户注册默认构造函数时,它们在搜索期间和调用 construct 成员函数时都是首选。

从 void 到 any

有时用户拥有的只是一个指向已知 meta 类型对象的不透明指针。在这种情况下,能够从中构造一个 meta_any 元素将非常方便。
为此,meta_type 类提供了一个 from_void 成员函数,旨在将不透明指针转换为 meta_any

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

不幸的是,无法对实际类型进行检查。因此,此调用可以被视为带有所有 问题静态转换 (static cast)
另一方面,从不透明指针构造 meta_any 的能力为一些值得探索的非常有趣的用途打开了大门。

策略:过犹不及

策略 (policies) 是一种编译时指令,可在注册反射信息时使用。
它们的目的是在某些特定情况下要求与默认行为略有不同的行为。例如,在读取给定数据成员时,其值被包装在 meta_any 对象中返回,默认情况下会对其进行拷贝。对于大型对象,或者如果调用者想访问原始实例,这种行为是不可取的。策略就是为了解决这个问题和其他问题而存在的。

目前有几种替代方案可用:

  • as-value 策略,与类型 entt::as_value_t 关联。
    这是默认策略。通常,不应显式使用它,因为如果未指定其他策略,则会隐式选择它。
    在这种情况下,函数的返回值以及作为数据成员公开的属性始终通过专用的包装器按拷贝返回,因此与它们的原始 meta 类型相关联。

  • as-void 策略,与类型 entt::as_void_t 关联。
    其目的是丢弃 meta 对象的返回值,无论它是什么,从而使其看起来好像其类型是 void

    entt::meta_factory<my_type>{}.func<&my_type::member_function, entt::as_void_t>("member"_hs);
    

    如果在函数中使用很明显,那么在构造函数和数据成员中使用可能就不那么明显了。在第一种情况下,即使仍然调用了构造函数,返回的包装器也始终为空。在第二种情况下,该属性无法被读取。

  • as-refas-cref 策略,与类型 entt::as_ref_tentt::as_cref_t 关联。
    它们允许构建充当未托管对象引用的包装器。访问包含在请求了 引用 的包装器中的对象,可以直接访问用于初始化包装器本身的实例:

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

    这些策略适用于构造函数(例如,当对象从外部容器获取而不是按需创建时)、数据成员和一般函数。
    如果一方面 as_cref_t 始终强制返回类型为 const,则 as_ref_t适应 传递对象的 const 属性以及返回类型的 const 属性(如果有)。

  • as-is 策略,与类型 entt::as_is_t 关联。
    用于将 meta 类型创建代码与调用代码解耦,同时仍保留数据成员和成员函数的定义行为:

    entt::meta_factory<my_type>{}.func<&my_type::any_member, entt::as_is_t>("member"_hs);
    

    对于返回引用类型的数据成员或成员函数,值按具有相同 const 属性的引用返回。在所有其他情况下,值按拷贝返回。

一些用法相当微不足道,但值得注意的是,存在一些不太明显的边缘情况,这些情况反过来可以通过使用策略来解决。

命名常量与枚举

如前所述,data 成员函数用于反射任何类型的常量。
这允许用户为枚举创建 meta 类型,其工作方式与从类构建的任何其他 meta 类型完全相同。同样,在需要时,算术类型会 丰富 具有特殊意义的常量。
由此导出的所有值对用户来说就像是反射类型的常量数据成员。这避免了直接在反射类型的空间中 导出 C++ 中枚举和类之间的区别的需要。

公开常量值或枚举中的元素非常简单:

entt::meta_factory<my_enum>{}
    .data<my_enum::a_value>("a_value"_hs)
    .data<my_enum::another_value>("another_value"_hs);

entt::meta_factory<int>{}.data<2048>("max_int"_hs);

访问它们也同样简单。只需执行以下操作,就像使用 meta 类型的任何其他数据成员一样:

auto value = entt::resolve<my_enum>().data("a_value"_hs).get({}).cast<my_enum>();
auto max = entt::resolve<int>().data("max_int"_hs).get({}).cast<int>();

所有这一切都在幕后发生,由于 meta_any 类执行的小对象优化 (small object optimization),没有任何内存分配。

用户定义数据

有时(例如,在创建编辑器时),将 traits 或任意 自定义数据 (custom data) 附加到创建的 meta 对象可能很有用。

它们之间的主要区别在于:

  • Traits 是简单的用户定义标志,具有更高的访问性能。库为 traits 保留最多 16 位,即 16 个标志用于位掩码,否则为 2^16 个值。
  • 自定义数据存储在为用户保留的通用快速访问区域中,库在任何情况下都不会使用该区域。

在所有情况下,此支持目前仅适用于 meta 类型、meta 数据和 meta 函数。

Traits

用户定义的 traits 通过 meta factory 设置:

entt::meta_factory<my_type>{}.traits(my_traits::required | my_traits::hidden);

在上面的示例中,使用了 EnTT 的 bitmask enum 支持,但任何整数值都可以,只要它不超过 16 位。
Traits 可以在不同时间分配。后续对 traits 函数的调用不会重置先前设置的值。但是,用户必须将 factory 重置为感兴趣的 meta 对象:

entt::meta_factory<my_type>{}
    .data<&my_type::data_member, entt::as_ref_t>("member"_hs)
    .traits(my_traits::internal);

创建后,所有 meta 对象都提供一个名为 traits 的成员函数来获取当前设置的值:

auto value = entt::resolve<my_type>().traits<my_traits>();

请注意,类型在注册时会被擦除,因此在 提取 traits 时必须重复该类型,以便允许库正确 重构 它们。

自定义数据

自定义任意数据通过 meta factory 设置:

entt::meta_factory<my_type>{}.custom<type_data>("name");

执行此操作的方法是将数据类型指定给 custom 函数,并传递必要的参数以正确构造它。
不可能在不同时间分配自定义数据。因此,对 custom 函数的多次调用会覆盖先前的值。但是,可以从 meta 对象读取此值,并使用 factory 更新现有数据,从而根据需要进行有效更新。
同样,如果需要,用户稍后也可以在 meta 对象上设置自定义数据,只要将 factory 重置为感兴趣的 meta 对象即可:

entt::meta_factory<my_type>{}
    .func<&my_type::member_function>("member"_hs)
    .custom<function_data>("tooltip");

创建后,所有 meta 对象都提供一个名为 custom 的成员函数,以引用或指向元素的指针的形式获取当前设置的值:

const type_data &value = entt::resolve<my_type>().custom();

请注意,返回的对象在转换为请求的类型之前会在 debug 模式下执行额外检查,以避免微妙的 bug。
只有在转换为指针的情况下,此检查才是安全的,并且会返回空指针以通知用户尝试失败。

注销类型

在反射系统中注册的类型也可以被 注销 (unregistered)。这意味着注销其所有数据成员、成员函数、转换函数等。但是,基类不会被注销,因为它们不一定依赖于它。
粗略地说,注销类型意味着断开所有关联的 meta 对象与其的连接,并使其标识符不再可用:

entt::meta_reset<my_type>();

也可以通过其唯一标识符重置类型:

entt::meta_reset("my_type"_hs);

最后,存在一个 meta_reset 函数的非模板重载,它不接受参数并一次性重置所有 meta 类型:

entt::meta_reset();

稍后可以使用完全不同的名称和形式重新注册类型。

Meta context

所有 meta 类型及其部分都在运行时创建并存储在默认 context 中。这是通过 service locator 获取的:

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

就其本身而言,context 是一个不透明的对象,用户能做的不多。但是,用户可以随时用另一个 context 替换现有的 context:

entt::meta_context other{};
auto &&context = entt::locator<entt::meta_context>::value_or();
std::swap(context, other);

这对于测试目的或定义多个具有不同 meta 类型的 context 对象以酌情使用非常有用。

如果 替换 默认 context 还不够,EnTT 还提供将多个外部管理的 context 与运行时反射系统一起使用的能力。
例如,要在默认 context 之外的 context 中创建新的 meta 类型,只需将其作为参数传递给 meta_factory 构造函数:

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

通过这样做,新的 meta 类型在默认 context 中不可用,但可以在需要时通过传递新的 context 来使用,例如在创建新的 meta_any 对象时:

entt::meta_any any{context, std::in_place_type<my_type>};

同样,要在默认 context 之外的 context 中搜索 meta 类型,必须将其传递给 resolve 函数:

entt::meta_type type = entt::resolve(context, "reflected_type"_hs)

更一般地说,当使用外部管理的 context 时,始终需要至少在 入口点 向系统提供要使用的 context。
例如,一旦获得了 meta_type 实例,就不再需要四处传递 context,因为 meta 类型会随身携带该信息并最终将其传播到其所有部分。
另一方面,当构造 meta_anymeta_handle、创建 factory 或解析 meta 类型时,必须指示库在哪里获取 meta 类型。