C++语言改造: 完全静态的反射机制
上篇文章我们提出了扩展C++反射功能的一些需求, 并且稍微谈了下实现的思路。现在我完全实现了静态反射的功能拓展,并且在代码生成器SAC(SakuraAutoCoder)中完成了静态反射相关代码的生成工作。先上仓库:
这是一个真正可用于生产环境, 并且完全运行时0 overhead的方案。
由于我使用了更新的语言特性(ISO C++20),这套方案从形式和优雅程度上,完爆Unreal。不过严格来说, Unreal也没有静态反射(实现全是动态的,功能基本是静态的)。欢迎各种形式的对标Unreal, 如果真的懂静态反射的老师读完觉得不太行,可以尽情挑刺。
阴阳话说的有点多, 那么下面这篇文章就来分析一下具体的实现细节,同时修正上篇文章成文时的一些纰漏。
静态反射的管辖范畴
在上篇文章我们简单提及了一些静态/动态反射相关的知识,但是对于静态反射的具体范畴, 当时的我存在一些误解。为了避免被人说不懂反射,这里进行一下概念上的纠正。
提及反射,很多时候大家喜欢讲:C++不支持静态反射。其实这种说法很不正确,准确来讲,C++不支持动态反射(因为根本没有运行时环境),同时静态反射非常孱弱。
任何程序自省的行为,都称作反射。举一个最简单的例子,typetraits中的std::is_xxx就是编译期程序自省的行为,是C++中静态反射功能集的组成部分。当然此种说法过于较真苛刻,一般在C++中谈及静态反射,我们都是指这个提案(以及它的修订版本):
对于C++来说倾向于使用推倒的形式获取类型信息,而不是从编译信息中直接获取信息。并且在编译时为字段添加信息的功能也完全没有,这就是需要我们自己进行扩展的内容了。我们需要:
在编译时进行字段/方法的遍历,这个遍历需要支持用户提供的filter;为字段/类提供静态的附加元信息,并且支持constexpr的getter,matching等操作
乍眼一看非常苛刻,但是完成这两条后静态反射会被完全补全,并且完全嵌入C++这个语言中。你可以在你想要的任何地方使用功能完全的静态反射,遍历域,获取meta信息,等等等等,并且这些操作都是constexpr的。
比如下面这个操作,我们遍历了TestComponent的词条:
瞅了瞅有没有char类型的字段,以及有没有字段含有container这个attribute。
检查一下我们的TestComponent,结果是完全正确且编译期完成的。
Parse
在Parse方面,暂时还是采用libclang。需要注意的是,完整的parse需要分析包含头,耗时可能会较长(尤其是当你包含了stl这些纯头的时候)。在包含了所有stl容器头的测试中,单个文件的parse耗时接近0.5s。不过考虑到我写的代码生成工具具有增量,姑且还是完全parse了。
这部分较好的方案是自己写一个特殊parser进行不完全的parse,可以大量减少等待时间。
CodeGen:干净且易读
我们谈回实现本身,大家都知道实现静态反射需要生成代码提供额外信息,但是该怎么生成,生成怎样的代码比较好呢?
我们先看一个负面例子,Unreal闪亮登场了。生成的大粪代码太多了,也简直不是人读的就不贴了。有兴趣可以自己去读,这里只说CodeGen的槽点:
为啥只有FXXX可以反射呢?反射部分的功能完全不需要依赖于任何类。生成的代码,序列化RPCGC各种东西混杂在一起,可见UHT就是一块铁板,不是引擎类就没人权。你不能分开生成吗?只能一次来一套全套大保健,这不是死脑筋吗;大兄弟你这宏也太多了,还用生成的宏套宏,没法读啊;全是动态的,生成了一堆XXXMap.Add,因为想用引擎容器装信息,C++版本和容器都太老了,InitializerList和constexpr全不支持,完了还不想重构;乱,乱,乱,太乱了。生成代码不用人读,你的代码生成环节需要维护扩展吧?这么乱这么没有规律,就算你成熟工具不需要维护,用户扩展的可能性都被抹杀了。
其中最大的漏洞,就是只支持FXXX,这是功能上的缺失。这里我们当然要规避掉,我们的反射支持所有的C++类和枚举。甚至所有的成员,我们生成的代码格式都高度一致,一视同仁。
解决了漏洞,我们解决设计上的问题。我们生成的代码,无论多少,必须有规律并且可以泛化处理。用户有啥依赖meta的需求,直接模板走起就可以拓展了。
静态反射的CodeGen图啥呢?不就是搜集信息吗。所以我们生成的代码就是纯粹的信息,对信息的操作都提供RuntimeHeader来实现。下面是生成的信息,格式高度一致且简洁:
特化一个模板来盛装反射类的信息。其中宏SFIELD_INFO展开后会生成一个类型,里面包含着对应域的各种信息。生成的类型,拥有着同名的成员符号,这为使用模板进行扩展提供了极大的便利。同时我们只包含constexpr信息,就可以只生成header,不用额外再链接一个生成的.cpp文件了。
我们的代码生成工具以后也会服务于其他的引擎功能(例如P/Invoke导出),但是会保持干净,生成的各个部分不会纠缠在一起,更不会生成万事屋型的对象代码。
Runtime支持:完全静态且纯头
要使用静态反射特性,利用起代码生成器生成的这些信息,需要一个头来进行支持。
对于field的遍历,我们直接对上面生成的tuple进行foreach访问即可。
透过lambda的类型推倒,也可以使用auto&&推倒出fieldinfo的类型,完成针对不同生成类型的泛用。
对于每个field,meta的类型都是不同的,但是不妨碍泛用
这个foreach在编译期就会完全展开,可以放心的在lambda内编写constexpr代码。
其实到此为止,我们必要的东西已经全部完成了。其余的部分,可以很方便的扩展得来,比如我们上文实现的那个filter,利用了constexpr std::string_view的一系列操作。
通过上面这两个matching函数,我们传入一个比较函数。把生成的信息数组unpack,利用unfold expression对它们逐个调用比较函数,并进行|操作。传入自定义的匹配函数,就可以在编译时对attributes进行matching了:
其余的扩展需求,都可以使用模板来进行类似的实现,这也是纯信息化系统的一大优势了。
页:
[1]