AutoCAD 3DMAX C语言 Pro/E UG JAVA编程 PHP编程 Maya动画 Matlab应用 Android
Photoshop Word Excel flash VB编程 VC编程 Coreldraw SolidWorks A Designer Unity3D
 首页 > C++

C++多态技术的实现和反思

51自学网 http://www.wanshiok.com



  两种不同的多态性应用场景

  学习过数值分析的读者应该熟知,在矩阵运算的电算求解领域,低阶稠密矩阵的求解与高阶稀疏矩阵的求解是性质完全不同的两个问题,其存储方案和求解算法截然不同。非常有趣的是,在多态性的实际应用中,也有着与矩阵问题类似的两种性质上截然不同的场景。

  第一种场景中,我们所构造的对象比较简单,同一族系中兄弟类总数不多,而彼此之间的差异较大,因此对象中的虚方法数量少,而改写率高。我们通常在教科书上所接触的面向对象例子,以及在一般应用领域中接触的对象都属此类。

  例如一个Modem类,即使其具有较多的特性,虚方法总数也很难超过20个,而不同的Modem类实现,可能会改写其中大部分甚至全部虚方法。另一个例子是COM接口。由于COM组件思想基于接口,而一个粒度良好的接口必然是“瘦小精干”的。比如IMalloc接口只有6个方法(不包括从IUnknown继承来的3 个方法),IPersistFile共5个方法,通常用户自己写的COM接口中的方法数量也不超过20。而在实现COM接口是,几乎总是需要改写全部方法。这与低阶稠密矩阵非常相似,因此值得用最简单直接的查找表结构来实现——速度快,而且简单直接。由于虚方法改写率高,查找表中的有效利用率较高。这种场景是C++多态性实现技术大大的用武之地,可以说C++特色的虚方法调用机制就是用来应对这种应用的。

  而第二种应用场景截然不同,在这种场景中,对象比较复杂,特性稠密,行为变化多端,同一族系中兄弟对象数量庞大,而彼此之间大同小异。此种对象中的虚方法数量多,而改写率低。GUI系统和视频游戏是这种应用场景的典型代表。由于我们整天与Windows 系统打交道,所以用Windows GUI系统来说明这种场景是最合适不过的了。我们知道,在Windows图形界面上的几乎所有实体从概念上讲都是Window对象,因此构成了一个对象族系。这个族系有三个突出的特点。一是行为多,特征多变(或者说虚方法数量多)。Microsoft Windows系统直接定义了数百个窗口消息,并允许用户使用WM_USER+n和WM_APP+n的方式定义新的消息,用面向对象的话来说,就相当于给Windows系统中的所有Window对象定义了数百个可供改写的虚方法,并且还允许用户自由扩展新的虚方法。

  第二个特点是改写率低,同族对象之间大同小异。通常我们对于绝大多数的窗口消息都是用DefWindowProc来统一处理,或者用SendMessage函数将消息转发(委托)给系统提供的标准窗口对象处理,这也就是相当于把这些消息交给基类窗口对象来处理,而只拦截(改写)其中几个至几十个消息(方法)。相对于窗口对象族庞大的虚方法数量来说,改写率通常不超过20%。第三个特点是同族兄弟类数量庞大。从标准窗口到异型窗口,从对话框到按钮,从工具条到文本框,所有的一切都是Window,甚至于两个按钮看上去完全一样,仅仅是caption不同,按下时执行的操作不同,就需要用不同的类来构造。因此在一个普通规模的应用程序GUI界面系统中,构造上百个大同小异的窗口类是并不奇怪的。任何一个对Win32 API有一定理解的开发者,对此都不难体会。

  从第1节对于C++虚方法调用机制的介绍可以很容易地知道,C++那种基于绝对位置的、不带任何自描述信息的查找表结构,并不适用于上述的第二种场景。如果强行使用C++原生的对象模型来实现类似Windows的GUI系统,那么结果是这样的:基类(不妨设为KWindow类)要定义1000个虚方法(其中应该留出多少位置供用户扩展之需呢?),从而拥有一个长达1000的查找表,而所有的直接和间接派生类对象,为了保持与KWindow 在方法查找表结构上的兼容,都要至少包容一个长达1000的查找表。

  我们举一个极端的例子来欣赏一下这种解决方案的荒谬性,假设有一个类KPushButton从KWindow中派生,并通过改写20个虚方法实现了一个标准的按钮控件,那么它的虚方法查找表中有多少项?对不起,不是20 项,而是至少1000项(如果它没有加入新的方法的话),其中绝大多数仅仅是KWindow虚方法表的原封不动的克隆,只有20项属于它自己,只有这20项真正有意义,方法表中980项被浪费掉了。它们唯一的意义在于占据一些位置,使得“指针加偏移”的计算能够继续准确地寻址。你以为事情已经很糟糕了?不,事实上还可以更糟糕!

  假设你需要一个标准按钮,它的外观、颜色、文字和其他行为都与KPushButton完全一样,仅仅是相应CLICK事件的操作不同,你需要怎么办?显然是从KPushButton中派生自己的KMyPush-ButtonOK类,然后改写其中的1 个方法(可能是叫做OnClick的)。那么在这个新的类中,虚方法表是多长呢?是1项吗?不是。是20项吗?也不是。实际上,是1000项!其中只有1项(OnClick)体现了它存在的意义,其他999项(在32位机器上占据3996个字节)几乎完全被浪费掉了!一个中等规模的应用程序中安排几十个界面,数百个自定制控件,则仅在虚方法表上浪费的存储空间即达到数百KB甚至1MB以上。也许这个数字在今天用GB 大筐装主存的时代实在是小儿科,但是其背后所体现的思路之丑陋却是任何一个有点良心的开发者(尤其是C++开发者)所不能容忍的。

  也正是因为这个原因,从OWL 到VCL,.. 从MFC到Qt,以至于近几年出现的GUI和游戏开发框架,所有涉及大量事件行为的C++ GUI Framework没有一家使用标准的C++多态技术来构造窗口类层次,而是各自为战,发明出五花八门的技术来绕过这个暗礁。其中比较经典的解决方案有三,分别以VCL 的动态方法、MFC的全局事件查找表和Qt 的Signal/Slot为代表。而其背后的思想是一致的,用Grady Booch的一句话来总结,就是:“当你发现系统中需要大量相似的小型类的时候,应当用大量相似的小型对象解决之。”2 也就是说,将一些本来会导致需要派生新类来解决的问题,用实例化新的对象来解决。这种思路几乎必然导致类似C#中delegate那样的机制成为必需品。可惜的是,标准C++ 不支持delegate。虽然C++社群里有很多人做了各种努力,应用了诸如template、functor等高级技巧,但是在效果上距离真正的delegate还有差距。因此,为了保持解决方案的简单,Borland C++Builder扩展了__closure关键字,MFC发明出一大堆怪模怪样的宏,Qt搞了一个moc前处理器,八仙过海,各显神通。

  让我们小结一下,面向对象多态性有两种不同的应用场景,而C++的标准多态技术只适合其中一种,对于另一种并不适合,必须以其他机制实现。

  解决思路和建议

  或许有读者读到这里,会对C++产生很大的怀疑。需要说明的是,C++选择的多态性实现技术是完全符合C++哲学的。而且,C++允许你以各种可能的办法来解决这个问题。时至今日,依靠各种成熟的GUI框架,大多数情况下我们可以自动绕过暗礁。

  问题的严重性在于,由于C++教育上的问题,很多开发者对于C++原生多态技术在上述第二种应用场合中的局限性认识不足,因此当他们面临类似的问题时,会不自觉地踏入陷阱中。在此我愿提醒C++开发者,当你面对的系统中含有标准的事件处理特征,而且事件数量较大时,请慎重考虑你的类层次结构设计。可以考虑模仿MFC或者Qt的解决方法,但在我看来,一个更加直接而且简单的方法是,模拟本文第1节中描述的、基于字符串比较的方法查找表,用一个单一的消息分发对象来向各个对象分发消息。由于这个消息分发对象会经常需要调整变化,将它单独放在一个DLL 甚至COM组件中,在运行时加载到进程内。这种方案不是最精巧的,但是在大多数情况下有效,并且实现起来比较简单。限于篇幅,这里不详细描述。

  事实上,我本人认为,C++语言应当从编译器上解决这个问题。基本思路为,当基类虚方法数量大而派生类改写的方法数量小的时候(这个信息可以从编译过程中得到),改变派生类对象的虚方法查找机制,改按位置查找为按被调用函数实际信息查找。这样一来,派生类中的虚方法表可不必与基类保持结构上的一致,从而避免了空间上的浪费。这种思路跟Delphi/Object Pascal语言中dynamic关键字有相似之处。本文不再赘述。

 
 

上一篇:C++的救赎 C++开源程序库评话  下一篇:C++的未来之路:C++0x概览