C# 子类实例化基类 基类使用不了子类的方法_C++ | 单继承化

a0833bdb017860dba666dc38395badb6.png

C++ 有多继承、虚继承、虚函数,但为了支持这些特性,代价是有的。比如

  • 多继承中,空基类之间需要 1 byte(历史原因)
  • 虚继承需要虚基类指针
  • 虚函数需要虚函数表指针

但是这些需求又是存在的,如何解决?

只要不使用多继承和虚继承就能解决前两个问题,第三个问题可以考虑 CRTP 的静态分派功能,虽然不完全等同。

本文介绍自创的一项技术,在不使用多继承和虚继承的情况下,依然能保持相应能力,同时支持 CRTP。这可视为新的 C++ 编程范式。

根本思路就是将继承关系转换为单继承,因此我将该技术命名为基于模板的单继承化,简称为单继承化(single-inheritize, SI)

项目地址

[ GitHub ] Ubpa | UTemplate​github.com

前文

Ubp.a:C++ | TemplateList​zhuanlan.zhihu.com

1. 手动单继承化

多个类之间的继承关系可分为虚继承(virtual inheritance)和非虚继承(non-virtual inheritance)。如果将类视为一个顶点,继承关系为边,则对应的图为有向无环图。示例如下(圆圈代表类,其中字母为类名,虚线代表虚继承,实线代表非虚继承,箭头起点为父类,终点为子类)

74cc4e85f4fabb0c08a6c0559704735e.png
继承关系示例

有向无环图可拓扑排序,如下

bd8c16a83624ce8dd69233525588062e.png
拓扑排序

因此单继承化是可行的,相应代码为

0b02e7b4cb4ed1cb65fe9aaed786171e.png
单继承化

2. 模板参数继承

第 1 节最后给出的代码没有灵活性,因为这些类可能又被其他类所用,如下

107c0929225f69d0046e4df218046b26.png

简单拓扑排序后代码为

a0819be313ab6e3e48023797b1184f85.png

可见 C 与第一节不同了,说明新需求下原有的拓扑关系而得到的代码无法适应新需求。

为解决这个问题,使用基类模板即可,上边两例相关代码可改为

0f28dee0ae9de716fd6ad154b4c2ee27.png

3. 依赖性

第 2 节我们将类设计成了基类模板类,但类是有依赖性的,这意味着不把依赖类写上会有问题,如下就有问题,因为 B 依赖于 A

2b99616f069b674b27d871ee2290bd54.png

因此作为类设计者,应该将其所依赖的类写明,这样调用者才不会缺漏

c78a0a263ead0a3b7de0d06f6b9585b2.png

这样类使用者可以根据他所要用的类,递归地获取信息从而得到类继承关系图,然后拓扑排序,然后根据排序结果依次套接各类。

4. 区分虚继承与非虚继承

之前我们忽略了虚继承和非虚继承的区别,但他们是不同的。

考虑下例

91a334f06e682b76fff8ad30555af5f9.png

一种拓扑排序为 HJBCAA,此时两个 A 在一起,我们无法得知 J 下的 A 到底是哪个。

另一种拓扑排序为 HBCAJA,此时我们可通过 H::J::A 访问到 JA,通过 H::AH::C::A 访问到虚基类 A

还有好几种拓扑排序,显然我们需要设计一种固定的策略。根据实践,应该选择的方案是将实继承的两者连续放置。并且构建拓扑排序时采取深度优先的策略,上边的 HBCAJA是该策略的一种结果,搜索路径为 H->J->A->J->H->B->A->B->H->C->A->C->H。这样我们能在 J 中通过 J::A(注意这里的 A 不是模板,而是类) 访问到 JA。 也就是说所有非虚继承的类,能直接通过自身的类名和实基类的类名访问到其实基类。

然而该如何访问虚基类呢?我们可以引入辅助类,在套接类的过程中同时收集好所有的虚基类。假设我们通过辅助类得到了所有的虚基类 AllVBs(里边存储的是实例类,而非模板),这样我们如果想在 B 中访问虚基类 A,则可通过 VBList 中得到。

这里涉及了很多很多模板操作,比如

  • 判断一个类型是否为特定模板的实例:is_instance_of<typename, template<typename...>class>
  • 辅助类的设计

以上不详细说明,第一点标准库没有,但有方法;第二点比较繁杂

5. 套接操作简化

假设我们得到的拓扑排序结果为 HBCAJA,那么就需要套接 6 个模板,我们不必要一个一个套接,可以稍微简化一下语法

b6c0443177cd211ac733649f2ea4845a.png

这也就是上节所说的辅助类,我们会一起处理虚基类和非虚基类,实际的模板为

f8233684a3565891e9388fae84fdfdf0.png

其中要完成 AllVBs 的生成,另外检查套接过程中基类是否含有当前类所需的虚基类,过程比较反复,设计改版过很多,这里不说明,但是确实可以做到。

这时我们的参数模板类实际为

3ce427489747132c85507f6afd5a7148.png

6. 静态拓扑排序

我们每次在使用类的时候,我们需要把所有虚基类找出来,并手动进行拓扑排序,并且这很麻烦,因此我们考虑是否有办法只提供直接基类(对 G 来说就是 EF)就可以得到拓扑有序的所有类,即

8e8c4b1dfe2d889bef0b1090ce535b26.png

这样我们就能轻松地写

3f8f7eb6cfcff2f6c8d96fadc8f0a915.png

算法很简单,就是深度优先后序遍历

用模板写的话,如下

d7b64bc8c0aaea4e4dedeac2bd870efe.png

7. 多模板参数

上边所有的参数模板类的模板参数只有 1 个,就是 Base,实际上是可以支持多个参数的。

为了支持整数,简单套一层模板即可

b7c82cfe0e21ad3fca29727c873d5987.png

我们这套编程范式是支持奇异递归模板模式(curiously recurring template pattern,CRTP)的,如下

cc92997a8339c6dc5e9a27d5d8130f4a.png

8. 演示

8.1 菱形继承

d9b63b9e8b85c9408603d7ffe72c46e4.png

04e8efad4e13139130b40faafce4648f.png

8.2 空基类优化

测试用例来源于 vs 开发团队[1],其所用优化方式为 __declspec(empty_bases),不跨平台。而单继承化编程范式也可以对此进行优化。

66c1ad0f20c1c0b11f96df0a45fb82bc.png

857f92630af35f5c2a9d78ea0b12cf5d.png

可以看到 naive 的方法有如下问题

  • 第 6 行的 Derived3 本应为 1 byte,但由于空基类多继承会有 1 byte 的padding,所以变成了 2 byte
  • 第 7 行的 Derived4 本应为 4 byte,但由于 int 的 alignment 是 4 byte,所以空基类多继承的开销也因为 alignment 变成了 4 byte。
  • 第 8 行的 Struct2 是 1 byte 的,但看其空基类 Empty1 的地址却是后 1 byte,也就是说对于一个该类的连续数组,某一位置的对象的 Empty1 成员的地址是下一个位置的对象。十分诡异

而通过单继承化编程范式,以上问题通通解决。而且比 vs2015 引入的__declspec(empty_bases) 更好的地方在于,不用管哪个类上该写这个,同时也跨平台。

8.3 CRTP

8ea2b8c1cecbda99a1489d2b26195abf.png
  • Vecf3 大小为 12 == 3 * sizeof(float),正常
  • 通过查汇编可知,最后两行的加法指令数相同,说明运行时这些空基类辅助类都能优化掉
  • IInIOut 组合得到 IInOutIAddIInOut 组合得到 IVal,灵活度极大,代码可复用性达到了单个函数的程度,通过简单写上所需接口即可,都是静态的,没有运行时开销,而且也没有类大小的开销
  • friend operator<<有些特殊,因为它依赖于 Impl 而不是依赖于模板类,导致如果用不同的模板参数时会导致重定义的问题。这种情况只发生于拓扑排序算法中,因为其中需要实例化模板,其中把Base设成了 NI_Nil,其他情况下不会引发问题。所以就简单偏特化了该情况避免了重定义问题。 这种问题只在 friend operator<<friend operator>> 中发生,后续可能可以更好解决。

9. 结语

该编程范式个人认为极好,使得我们可以将一切函数拆成单函数接口,然后用组合方式进行组装,表达能力上更强。原本 C++ 就有这种能力,但由于空基类多继承,虚继承会引发极大量的 padding,因此只能是一个接口包含大量内容,从而使得多接口继承的额外开销忽略不计。

本套编程范式直接优化了 C++ 底层,从而增强了语言机制,应用有待开发。本人近期将以此重写数学库。

感谢大家的阅读。

参考

  1. ^Optimizing the Layout of Empty Base Classes in VS2015 Update 2 https://devblogs.microsoft.com/cppblog/optimizing-the-layout-of-empty-base-classes-in-vs2015-update-2-3/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值