02 - 实现反射
尝试从零实现(抄)一个反射系统,并应用于我酝酿中的游戏引擎项目里!本节将实现反射系统的第二部分:实现反射代码,基于第三方EnTT库中的反射系统。
没怎么看明白。
EnTT库
简介
EnTT库是一个单头文件,小巧易用的游戏编程库,使用现代C++实现。使用该库的公司有《我的世界》的Mojang等。该库包含ECS组件,反射组件,资源管理组件等,是一个实用工具集合。
要想使用EnTT,需要包含头文件<entt/entt.hpp>
。如果只用它的ECS系统,仅需包含头文件<entt/entity/registry.hpp>
。
反射系统
EnTT库提供的反射库是内建,非侵入且无宏的动态反射库。
名称与标识符
当用户处理名称和标识符时,元信息系统不强迫它们使用库提供的工具。也就是说,用户可以为元对象分配任何类型的标识符(只要它们是数字)。
// 标识符为EnTT库内置的字符串哈希
entt::meta_factory<my_type>{}.type("reflected_type"_hs);
meta_factory
要使用反射系统反射一个类,首先得实例化一个meta_factory
:
entt::meta_factory<my_type> factory;
工厂会将被反射类的信息提供给反射系统使用。它的所有成员函数都返回它本身,通常被用来构建如下内容:
构造函数:构造函数通过指定参数列表被分配给反射类型。如果返回类型符合预期,自由函数也会被接受。从客户端的角度来看,自由函数和实际构造函数之间没有任何变化:
entt::meta_factory<my_type>{} .ctor<int, char>() .ctor<&factory>();
如有可能,元对象的默认构造函数被隐式生成。
析构函数:自由函数和成员函数都是合法的虚构函数:
entt::meta_factory<my_type>{} .dtor<&destroy>();
析构函数的目的是提供释放资源的可能,在一个对象被实际摧毁前。一个函数应该从不删除或显式调用所给实例的析构函数。
数据成员:元数据成员是被反射类型的实际数据成员,也能是静态或全局变量,还有常量。从客户端角度来讲,所有变量都和被反射类型相关联,就像类型本身的一部分:
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()
需要为元数据成员使用的标识符。有了标识符,用户就能在运行时通过名字访问它。也能通过一组
Setter
和Getter
来定义数据成员,这种方法对于从非const
数据成员创建只读属性来说很方便:entt::meta_factory<my_type>{} .data<nullptr, &my_type::data_member>("member"_hs);
成员函数:可以是被反射类的成员函数,也能是普通函数。从客户端角度来说,这些函数就是被反射类本身的一部分:
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()
需要为元数据函数使用的标识符。用户可以在运行时通过名字来访问它。元函数的重载受支持,在运行时它们会被反射系统根据入参自动解析,下例来自官方测试文件
entt/test/entt/meta/meta_func.cpp
:// 如果entt::overload<>导致编译器内部错误, 可在官方Wiki Core处找到替代方案 // .func<static_cast<int(Class::*)(int, int)>(&Class::f)>("f"_hs) entt::meta_factory<function>{} .func<entt::overload<int(int, int)>(&function::f)>("f"_hs) .func<entt::overload<int(int)const>(&function::f)>("f"_hs); ASSERT_TRUE(type.invoke("f"_hs,instance,0)); ASSERT_TRUE(type.invoke("f"_hs,instance,0,0));
基类:就是被反射类的父类:
entt::meta_factory<derived_type>{} .base<base_type>();
反射系统会记录类的继承关系,并允许运行时进行隐式转换。也就是说,当
base_type
被需要时,一个derived_type
实例也可被接受。转换函数:转换函数允许用户定义一些转换,将会在反射系统中必要隐式转换时用到:
entt::meta_factory<double>{} .conv<int>();
meta_type
注册好反射信息后,就能在运行时使用反射了。要想获取某类的反射信息,有三种方式:
// 根据类型获取
auto by_type = entt::resolve<my_type>();
// 根据标识符获取
auto by_id = entt::resolve("reflected_type"_hs);
// 根据entt::type_info获取
auto by_type_id = entt::resolve(entt::type_id<my_type>());
要想获取反射系统中的所有反射信息,可以直接调用entt::resolve()
:
for(auto &&[id, type]: entt::resolve()) {
// ...
}
上面返回的都是meta_type
实例(可能还有id)。接下来看看如何利用它获取被反射类的详细信息:
元数据成员:
auto data = entt::resolve<my_type>() .data("member"_hs);
返回的类型是
meta_data
,或者非法值。一个meta_data
对象会提供一些查询API,可以获得数据成员的类型和值。元函数成员:
auto func = entt::resolve<my_type>() .func("member"_hs);
返回的类型是
meta_func
或非法值。一个meta_func
对象会提供一些查询API,可以获得函数的类型,参数个数等信息。还能调用函数并获得meta_any
类型的返回值。
这些meta_xxx
对象都能被显式转换为bool
值,以检查它的合法性:
if(auto func = entt::resolve<my_type>()
.func("member"_hs);
func)
{
// ...
}
此外,meta_type
也被用于创建被反射类的实例。它的.construct()
成员函数接受用于构造被反射实例的参数列表。如果传参和被反射类的构造函数匹配,就会返回一个合法的meta_any
,否则会返回一个没有初始化的meta_any
。
meta_any
这是一个存储任意类型的对象,可用entt::forward_as_meta()
或std::in_place_type<T&>
来将一个特定的类型告知它。如果要将它转换为特定的类型,可用成员函数try_cast()
,cast()
和allow_cast()
。
使用allow_cast()
的示例如下:
entt::meta_any any{42};
any.allow_cast<double>();
double value = any.cast<double>();
// 转换枚举类型
entt::meta_type type = entt::resolve<int>();
entt::meta_any any{my_enum::a_value};
any.allow_cast(type);
int value = any.cast<int>();
反射常量和枚举
枚举和常量也能被反射:
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);
访问被反射的枚举和常量的例子如下:
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_sequence_container_traits
类或meta_associative_container_traits
类的特化。
EnTT已经特化了一些常用容器:
- 顺序容器:
std::vector
,std::array
,std::deque
和std::list
(不是std::forward_list
); - 关联容器:
std::map
,std::set
和它们的unordered
版本;
要让这些特化对编译器可用,需要包含头文件<container.hpp>
。
假设有个容器类是meta_sequence_container_traits
的特化,那么反射系统将视其为顺序容器。可以通过如下方式来访问它:
std::vector<int> vec{1, 2, 3};
entt::meta_any any = entt::forward_as_meta(vec);
if(any.type().is_sequence_container()) {
// 获取访问代理view, 并验证是否合法
if(auto view = any.as_sequence_container(); view) {
// 通过view遍历容器里元素
for(entt::meta_any element: view) {
// ...
}
}
}
同理,访问关联容器需要使用as_associtaive_continer()
。从上述代码可以看出,访问容器的类型是根据上下文转换的,我们要访问它时类型为meta_xxx_container
,要验证它的合法性时会被自动转换为bool
。
接下来看看顺序容器meta_sequence_container
的API:
value_type()
:返回容器内元素的meta_type
;size()
:返回容器内元素的个数;resize()
:尝试对容器进行重置大小操作,成功返回true
;clear()
:尝试清理掉容器内元素,成功返回true
;reserve()
:尝试为容器保留一定的容量,成功返回true
;begin()
和end()
:返回容器对应迭代器;insert()
:往容器中加入元素,接受一个元对象迭代器和一个元素作为参数;auto last = view.end(); view.insert(last, 42);
返回指向被插入元素的调度器和指示插入操作是否成功的布尔值(通过上下文转换实现)。
erase()
:移除容器中的元素,接受一个元对象迭代器作为参数:auto first = view.begin(); // removes the first element from the container view.erase(first);
operator[]
:和数组访问一样,返回meta_any
;
然后是关联式容器meta_associative_container
的API:
key_only()
:如果容器只有key
没有value
,返回true
;key_type()
:返回key
的meta_type
;mapped_type
:对于只有key
的容器返回不合法的meta_type
;对于其他类型的容器返回被映射值的meta_type
;value_type
:返回容器内元素的meta_Type
。例如,容器是std::set<int>
,就会返回int
;容器是std::map<int, char>
,就会返回std::pair<const int, char>
。size()
:返回容器内元素的个数;resize()
:尝试对容器进行重置大小操作,成功返回true
;clear()
:尝试清理掉容器内元素,成功返回true
;reserve()
:尝试为容器保留一定的容量,成功返回true
;begin()
和end()
:返回容器对应迭代器;insert()
:往容器中添加元素,接受一个迭代器,一个key
值和一个value
值作为参数:auto last = view.end(); // appends an integer to the container view.insert(last.handle(), 42, 'c');
erase()
:用于移除容器中的值,参数为要移除的key
;operator[]
:和std::map
访问一样,接受key
作为查询索引,返回meta_any
;
指针类型支持
可告知反射系统哪个类型是指针,这需要用户特化is_meta_pointer_like
类。EnTT已经为常用的类进行特化,包括:
所有普通指针类型;
std::unique_ptr
和std::shared_ptr
;所有导出
is_meta_pointer_like
类型的类:struct smart_pointer { using is_meta_pointer_like = void; // ... };
要让这些特化对编译器可用,需要包含头文件<entt/pointer.hpp>
。使用指针类型的例子如下:
int value = 42;
// meta type equivalent to that of int *
entt::meta_any any{&value};
if(any.type().is_pointer_like()) {
// meta type equivalent to that of int
if(entt::meta_any ref = *any; ref) {
// ...
}
}
模板信息支持
还支持反射模板信息,不过我对模板这块不怎么清楚,且目前没需求,就不看了。
自定义附加信息
还能通过meta_factory
对反射类附加自定义信息:
struct my_data
{
int intVal;
char charVal;
std::string strVal;
};
// 通过factory.custom<类型>(构造参数)来附加自定义信息
entt::meta_factory<int>{}
.custom<my_type>(1, '2', "3");
// 通过resolve<类型>.custom()来获取自定义信息
const my_data& data = entt::resolve<int>().custom();
需要注意的是,重复附加自定义信息会覆盖掉之前的信息,构造参数必须和类型相符,否则会编译不通过。
注销类型
可通过meta_reset()
来从运行时反射系统中注销类型:
// 通过类型名注销
entt::meta_reset<my_type>();
// 通过类型标识符(如字符串哈希)注销
entt::meta_reset("my_type"_hs);
// 注销所有已反射类型
entt::meta_reset();
meta_context
运行时反射系统中的所有反射信息被存储在默认meta_context
中,可通过locator
获取:
auto &&context = entt::locator<entt::meta_context>::value_or();
通过meta_factory
给其他meta_context
中添加反射信息的示例如下:
entt::meta_ctx context{};
entt::meta_factory<my_type>{context}.type("reflected_type"_hs);
既然有“默认meta_context
”这一概念,就说明在运行时可灵活切换多个meta_context
。切换meta_context
的示例如下:
entt::meta_context other{};
auto &&context = entt::locator<entt::meta_context>::value_or();
std::swap(context, other);
除了切换默认meta_context
外,还能在未切换的状态下直接使用其他meta_context
:
entt::meta_any any{context, std::in_place_type<my_type>};
entt::meta_type type = entt::resolve(context, "reflected_type"_hs)
实践
接下来开始利用EnTT::meta
库封装自己的反射库,详见我的毕业设计中Engine/Runtime/Core/Reflection
部分。
参考资料
- BoomingTech/Piccolo: Piccolo (formerly Pilot) – mini game engine for games104 (github.com)
- skypjack/entt: Gaming meets modern C++ - a fast and reliable entity component system (ECS) and much more (github.com)