« 在 C++ 中实现一个轻量的标记清除 gc 系统 | 返回首页 | 在 C++ 中引入 gc 后的对象初始化 »

C++ 中的接口继承与实现继承

为这篇 blog 打腹稿的时候,觉得自己很贱,居然玩弄 C++ 起来了。还用了 template 这种很现代、很有品味的东西。写完后一定要检讨。

起因是昨天写的那篇关于 gc 的框架。里面用了虚继承和虚的析构函数。这会导致 ABI 不统一,就是这个原因,COM 就不用这些。

说起 COM ,我脑子里就浮现出各种条条框框。对用 COM 搭建起来的 Windows 这种巨无霸,那可真是高山仰止。套 dingdang 的 popo 签名:虽不能至,心向往之。

好吧,我琢磨了一下如何解决下面的问题,又不把虚继承啦,虚析构函数啦之类的暴露在接口中。

简单说,我有几个接口是一层层继承下来的,唤作 iA iB 。iA 是基类,iB 继承至 iA 。

然后,我写了一个 cA 类,实现了 iA 接口;接下来我希望再写一个 cB 类,实现 iB 接口。但是,iB 接口的基类 iA 部分,希望复用已经写好的 cA 类。我想这并不是一个过分的需求。正如当年手写 COM 组件时,我对手写那些 AddRef Release QueryInterface 深恶痛绝。

用虚继承可以简单的满足这个需求:

#define interface interface iA { virtual void foo() = 0; }; interface iB : virtual public iA { virtual void bar() = 0; }; class cA : virtual public iA { virtual void foo(); // etc... }; class cB : virtual cA , virtual public iB { virtual void bar(); // etc... };

每当我创建一个 cB 对象,并返回 iB 接口指针时,cB 对象内部的继承关系,可以用个简单的图表示如下:

iB  +- cB
|      |
+      +
iA  +- cA

但是,我们在公开的 iX 接口定义中,使用了虚继承。这在不同编译器上可能有一些差异。如果组件写好后,动态库想拿给别人用,无法预知别人用的编译器,就会有问题。

嗯,这可能是个伪命题。或许不需要解决。但感觉最近有点犯贱。那么下面就讨论一下:怎么不在接口定义中使用 virtual 继承,而达到同样的目的呢?

我的方法是用 template 做一个中间层,然后手工写一些转发代码:

#define interface struct interface iA { virtual void foo() = 0; }; interface iB : public iA { virtual void bar() = 0; }; class cA : public iA { public: virtual ~cA(); virtual void foo(); // etc... }; template< typename IF > class tA : cA , public IF { virtual void foo() { cA::foo(); } }; class cB : tA { virtual void bar(); // etc... };

这样,模板 tA 解决了多继承后,接口实现的转发问题。(因为手工转发了 foo 的调用)

既然要转发,为什么要用多继承而不用组合呢?因为我需要 ~cA 正确的发挥作用。比如在 cA::foo 中可以正确的 delete this 。

下一个问题:如果还有一个 iC 继承于 iB ,然后在实现 cC 的时候,想复用 cB ,同时不想写太多的 api 转发代码。怎么办?

我初步的想法是,把 tA 模板的实现都改成虚继承,然后做一个 tB 模板。转发 cB 扩展的几个 api 。然后让 cC 从 tA tB 虚继承出来。随手写了一个,但编译有点问题,不想深入研究了。弄出个模板虚拟多继承,绝对是蛋痛啊。

研究 C++ 果然是浪费生命。

Comments

这种模版方式的实现在游戏中UI控件上应用其实非常常见:

一般的控件都会继承自IControl,
其他一些控件如Container和Dialog分别抽象出IContainer和IDialog,IContainer由CDialog类来具体实现;IDialog由CDialog类来具体实现。

但是IDialog需要继承自IContainer;同时CDialog又要继承自CContainer,这种时候恐怕必须得采用这种模版方式来实现了。

其实有时候很讨厌用模版的……

这种模版方式的实现在游戏中UI控件上应用其实非常常见:

一般的控件都会继承自IControl,
其他一些控件如Container和Dialog分别抽象出IContainer和IDialog,IContainer由CDialog类来具体实现;IDialog由CDialog类来具体实现。

但是IDialog需要继承自IContainer;同时CDialog又要继承自CContainer,这种时候恐怕必须得采用这种模版方式来实现了。

其实有时候很讨厌用模版的……

我在写游戏时,根据自己的经验就是起初在构造函数里写点数据初始化,后来,就写个空函数放那里,再后来那个空函数都去掉,因为我发现那个空的构造函数都可能在退出程序时崩溃.
云风退出C++用稍微有点极端,其实很多用C++的都悟出解决方法了,像用C一样用C++.
再提一点,我连指针现在都尽量不用,除非是很大的对象要在运行时删除,减少内存,不然一律直接申明.
C++里的一些复杂语法简直是那些天天研究语法硬憋出来的,我极其鄙视模板,除了增加语法复杂度,极其难调试,模板根本就是废物.
还有现在在设计架构时,考虑的面向数据,面向对象是伪命题我同意这个说法.
对于公司来说,松耦合,绝对的模块化是必须的,C++那些语法都是越搞越强的耦合.

博文没有上一篇下一篇按钮啊

就这里的情况来说,这些模版只会暴露给dll内部,所以影响的范围有限。如果只是个2、3千行甚至小得多的dll,那么这种暴露应该危害不大。特别是很可能这些类只是调用了其他一些非模版类的接口,那样这些模版类里的实现会有多少代码也不一定了,可能并不会很多。

放在 template 里没什么特别的问题。

我太古板,不喜欢把实现都暴露出来。所以本能的排斥这种做法。

btw, 我手头有一篇 google 内部的文章,Mike Burrows 写的 Abstraction and specification in large software projects

观点和我类似。他还反对 C++ 的构造函数等等东西。

又加了几层,最后有5层,没出问题。

具体功能实现在模版里会有什么问题吗……

把具体功能实现在 template 里,我认为是件很糟糕的事情。

在一个exe内试了一下,没试在dll之间的传递。没遇到什么问题,要不就是有些情况没有试到。

#include <iostream>

using namespace std;

class IA
{
public:
virtual void a(void) =0;
};

class IB : public IA
{
public:
virtual void b(void) =0;
};

template< class Base >
class CA : public Base
{
public:
void a(void)
{
cout << "CA::a" << endl;
}
};

template< class Base >
class CB : public CA< Base >
{
public:
void b(void)
{
cout << "CB::b" << endl;
}
};

int main()
{
CA< IA > a;
CB< IB > b;

IA* ia= &a;
IB* ib= &b;
IA* ia4b= &b;

ia->a();
ib->b();

ia4b->a();

return 0;
}

@sjinny

你不妨试试,我先就是这样做的。有些问题没有解决。就没放在 blog 里。

这个要自己实作才会发现 :)

第一次留言,占个位,哈哈

高级特性,用用,还是很有意思的,但是究竟是不是符合实际需要,又是一回事了

还是很佩服云风的,沉迷于Cpp的高级特性时,云风已经超越了.

如果这样呢:
class IA {};
class IB : public IA {};

template< class Base >
class CA : public Base {};

template< class Base >
class CB : public CA< base > {};

生成单独的CA的实例的时候,使用CA< IA >,生成单独的CB的实例的时候,使用CB< IB >。这两种对象都可以转为IA,后者可以转为IB。
可以把IA、IB、IC、ID这些理解为一个接口链,它们继承为一个线性结构,对应的实现CA、CB、CC、CD也是继承为一个线性的接口,是实现链或者叫功能链,只不过这个链子的最前面的基类会从一个模版参数类派生。要把接口链和实现链连接起来,就要把接口链的最后一个类作为实现链的第一个类的基类。
不过这些还没有考虑到更复杂的继承情况。

个人不是完全认同,挺特例独行的

看过这篇blog,恕我直言,感觉一些用词让你很难成为大师级人物。并无他意,感觉你有那么好的基础和过去,可以成就更高更大的东西

除了略干条,和一楼差不多。

我觉得这就是用c++最让人难受的问题之一,我一般竭力避免接口之间互相继承。


我在c和c++之间徘徊过好几次,最后选择像使用c一样使用c++
纠结于c++各个高级特性之间错综复杂的关系,实在是头疼

函数重载:非常有用,避免了很多后缀标记
结构体继承:非常有用,带来语法上的便捷
模板:比较有用,和宏互补,是代码生成的工具

构造函数、析构函数:不使用,显示的构造、析构控制性更强,看起来更清楚
虚函数:不使用,使用它就必须使用构造函数了,我自己设计了一个统一的对象模型,也不需要使用c++语法的虚函数
异常:不使用,异常安全通常和构造函数析构函数联系在一起,setjmp/longjmp只需保存恢复6个寄存器,非常高效
RTTI:不使用,我设计的统一的对象模型有自己的机制
private、protected、public:不使用,能结构体继承足够了

Post a comment

非这个主题相关的留言请到:留言本