C++单类定义多接口
最近做ThoughtWorks的面试题,第一遍用最简单的方法实现了,面试官说没有OO设计,于是乎想炫一波OO设计,结果才发现,原来C++还差的很远。先来说说这个多接口的坑。还会有另外的笔记阐述:
- 运算符重载与拷贝构造函数
- 菱形继承
- 函数中的按引用调用与返回
- 以后想到再加
C++中的接口
接口是面向对象很重要的概念。按照《敏捷软件开发-原则、模式与实践》一书所述,所有软件高层不应依赖于低层,而应依赖于抽象出的接口。这就是依赖倒置原则(DIP)。Java中的接口用interface
定义,C++中没有提供专门的关键字。而是用纯虚函数来表示接口。形如:
class interface{public: virtual void func() = 0;}
接口是不能实例化的,因为包含了纯虚函数。
单类定义多接口
在Java中很容易做到,只需class A implement IA, IB{}
即可。在C++中可否这么做呢?很容易就想到C++中的多继承,如下:
class IA {public: virtual void funcA() = 0;}class IB {public: virtual void funcB() = 0;}class C : public IA, public IB { void funcA(); void funcB();}
按照最初的设想,我希望能够:
C* c = new C();IA* pA = (IA*)c;IB* pB = (IB*)pA;pA->funcA(); // 调用A函数pB->funcB(); // 调用B函数
但我发现,现实与理想差得很远。pB->funcB()
调用的其实也是funcA()
。原因与多继承内存分布与类型转换有关。
在发生多继承时,一个C对象中内存分布如下,盗用陈浩的博客C++ 对象的内存布局中的图:
Base1* pA = (Base1*)derive;Base2* pB = (Base2*)derive;
当发生向上类型转换时,编译器均能很好的工作,不用依赖于各种强制转换,也可以识别到正确的函数。但是如果发生非正常的类型转换时,就会遇到问题。例如前文所述的将Base1
型指针直接转型到Base2
就会出问题。可参考这一篇博文:当C++多继承遇上类型转换。文中提到了三个解决方案,我尝试了前两个,分别用static_cast
和dynamic_cast
均不能解决。奇怪的是,dynamic_cast
的解决方案看起来在网上确是颇有一些支持者,看下面的链接:
- Multiple inheritance casting from base class to a different derived class
- Type casting to an interface (Abstract class) in case of Multiple Inheritance in C++
- How to perform casting with multiple inheritance
在我自己的code中,dynamic_cast
之后的结果是虽然没有抛出bad cast异常,但是转换的结构是NULL。
解决方案
上面提到的方案,都不成功,但最终我还是找到了能解决的办法,尽管看起来不像想象中优雅。
方案一:采用菱形继承
根据查阅的各种网站资料,这样的继承会增加内存布局,以及调用的复杂度。但确实能解决上面多接口转型的问题。具体代码如下:
class IBaseclass IA : virtual public IBase {}class IB : virtual public IBase {}class C : public IA, public IB {}IA* pA = new C();IBase* base = (IBase*)pA;IB* pB = dynamic_cast<IB*>(base);
这样的做法,让人如鲠在喉,一个简单的调用,搞的这么复杂。转来转去,可读性极差,而且dynamic_cast
还有性能问题。
而且这里还会遇到source type is not polymorphic的问题,解决方法使添加虚析构函数。
class IBase { virtual ~IBase(){};}class IA : virtual public IBase {}class IB : virtual public IBase {}class C : public IA, public IB {}IA* pA = new C();IBase* base = (IBase*)pA;IB* pB = dynamic_cast<IB*>(base);
方案二:采用单接口
这一方案,是受了下面的链接的启发:Implementing multiple interfaces in a single C++ class
As you found out, you cannot implement two interfaces with one single C++ class directly. However, you could simply define a new Slice interface derived from the two other Slice interfaces, and then implement this new Slice interface with a single C++ class.
作者说C++中直接定义多接口并不是一个好的实现,定义一个虚接口类来继承两个虚接口,实现类直接继承自这个虚类。代码如下:
class IA {}class IB {}class IAB : public IA, public IB {}class C : public IAB {}
这样转型起来就方便多了
C* c = new C();IA* pA = (IA*)c;IB* pB = (IB*)pA; // 仍会出错,不能直接转IB* pB2 = (IB*)(IAB*)pA; // 可以工作
但其实上层的代码,应该依赖于IAB
,而不应该也不能够只依赖于IA
, IB
,而对IAB
一无所知。
总结
- 在设计接口的时候,不要指望用多继承来模仿Java里的多接口定义。
- 尽量避免在多继承的时候,需要在没有继承关系(
IA
&IB
)之间进行指针转型。虽然菱形继承+dynamic_cast
可以做到,但是可读性和性能会下降。 - 可以用方案二中的方法来实现近似的多接口定义。这是一个相对简介有效的方法。