RTTI(Run-Time Type Identification)、虚函数和虚基类的实现方式、开销分析及使用指导
为了说明RTTI、虚函数和虚基类的实现方式,这里首先给出一个经典的菱形继承实例,及其具体实现(为了便于理解,这里故意忽略了一些无关紧要的优化):
图中虚箭头代表偏移,实箭头代表指针
由上图得到每种特性的运行时开销如下:
可见,关于老天“饿时掉馅饼、睡时掉老婆”等美好传说纯属谣言。但凡人工制品必不完美,总有设计上的取舍,有其适应的场合也有其不适用的地方。 C++中的每个特性,都是从程序员平时的生产生活中逐渐精化而来的。在不正确的场合使用它们必然会引起逻辑、行为和性能上的问题。对于上述特性,应该只在必要、合理的前提下才使用。 "dynamic_cast" 用于在类层次结构中漫游,对指针或引用进行自由的向上、向下或交叉强制。"typeid" 则用于获取一个对象或引用的确切类型,与 "dynamic_cast" 不同,将 "typeid" 作用于指针通常是一个错误,要得到一个指针指向之对象的type_info,应当先将其解引用(例如:"typeid(*p);")。 一般地讲,能用虚函数解决的问题就不要用 "dynamic_cast",能够用 "dynamic_cast" 解决的就不要用 "typeid"。比如:
以上代码用 "dynamic_cast" 写会稍好一点,当然最好的方式还是在CShape里定义名为 "rotate" 的虚函数。 虚函数是C++众多运行时多态特性中开销最小,也最常用的机制。虚函数的好处和作用这里不再多说,应当注意在对性能有苛刻要求的场合,或者需要频繁调用,对性能影响较大的地方(比如每秒钟要调用成千上万次,而自身内容又很简单的事件处理函数)要慎用虚函数。 需要特别说明的一点是:虚函数的调用开销与通过函数指针的间接函数调用(例如:经典C程序中常见的,通过指向结构中的一个函数指针成员调用;以及调用DLL/SO中的函数等常见情况)是相当的。比起函数调用本身的开销(保存现场->传递参数->传递返回值->恢复现场)来说,一次指针间接引用是微不足道的。这就使得在绝大部分可以使用函数的场合中都能够负担得起虚方法的些微额外开销。 作为一种支持多继承的面向对象语言,虚基类有时是保证类层次结构正确一致的一种必不可少的手段。但在需要频繁使用基类提供的服务,又对性能要求较高的场合,应该尽量避免使用它。在基类中没有数据成员的场合,也可以解除使用虚基类。例如,在上图中,如果类 "BB" 中不存在数据成员,那么 "BB" 就可以作为一个普通基类分别被 "B1" 和 "B2" 继承。这样的优化在达到相同效果的前提下,解除了虚基类引起的开销。不过这种优化也会带来一些问题:从 "DD" 向上强制到 "BB" 时会引起歧义,破坏了类层次结构的逻辑关系。 上述特性的空间开销一般都是可以接受的,当然也存在一些特例,比如:在存储布局需要和传统C结构兼容的场合、在考虑对齐的场合、在需要为一个本来尺寸很小的类同时实例化许多对象的场合等等。 |