RTTI(Run-Time Type Identification)、虚函数和虚基类的实现方式、开销分析及使用指导

来源:互联网 发布:小米2s3g网络设置 编辑:程序博客网 时间:2024/04/28 12:53

RTTI(Run-Time Type Identification)、虚函数和虚基类的实现方式、开销分析及使用指导


为了说明RTTI、虚函数和虚基类的实现方式,这里首先给出一个经典的菱形继承实例,及其具体实现(为了便于理解,这里故意忽略了一些无关紧要的优化):


图中虚箭头代表偏移,实箭头代表指针

由上图得到每种特性的运行时开销如下:
 特性时间开销空间开销RTTI几次整形比较和一次取址操作(可能还会有1、2次整形加法)每类型一个type_info对象(包括类型ID和类名称),典型情况下小于32字节

 

虚函数一次整形加法和一次指针间接引用每类型一个虚表,典型情况下小于128字节

每对象若干个(大部分情况下是一个)虚表指针,典型情况下小于8字节

 

虚基类从虚继承的子类中访问虚基类的数据成员或其虚函数时,将增加两次指针间接引用和一次整形加法(部分情况下可以优化为一次指针间接引用)。每类型一个虚基类表,典型情况下小于32字节

每对象若干虚基类表指针,典型情况下小于8字节

在同时使用了虚函数的时候,虚基类表可以合并到虚表(virtual table)中,每对象的虚基类表指针(vbptr)也可以省略(只需vptr即可)。实际上,很多实现都是这么做的。这样做的缺点是需要为一些中间类型(如:B1、B2 等)准备多个虚表。

如果指定类型在其类层次结构中只有一个虚基类(大部分使用了虚基类的情况下都是如此,如:上例中就只有 BB 一个虚基类),则可将 vbptr 直接替换为虚基类的偏移地址,这样做将可节省一次指针间接引用,从而提高效率。很多编译器都会自动开启这类优化措施。

此外,由于在很多原本需要访问虚表内 offset 字段的场合中(例如:调用某些虚函数时),该值都是编译时已知的。此时只需一个整形立即数加法即可完成从基类对象到派生类 this 指针的转换。因此,在不怎么影响时间效率的前提下,可以仅保留一个 vbptr 指针(意即:上例中 B2 内的 vbptr 可以被省略)。这种优化方式常常与前文提到的,在单虚基类的场合中将 vbptr 直接替换为虚基类偏址的做法一同使用,以期在时间效率和空间效率间取得较好的平衡,例如:VC 就经常使用这样的优化方式。

 

 * 其中“每类型”或“每对象”是指用到该特性的类型/对象。对于未用到这些功能的类型及其对象,则不会增加上述开销

可见,关于老天“饿时掉馅饼、睡时掉老婆”等美好传说纯属谣言。但凡人工制品必不完美,总有设计上的取舍,有其适应的场合也有其不适用的地方。

C++中的每个特性,都是从程序员平时的生产生活中逐渐精化而来的。在不正确的场合使用它们必然会引起逻辑、行为和性能上的问题。对于上述特性,应该只在必要、合理的前提下才使用。

"dynamic_cast" 用于在类层次结构中漫游,对指针或引用进行自由的向上、向下或交叉强制。"typeid" 则用于获取一个对象或引用的确切类型,与 "dynamic_cast" 不同,将 "typeid" 作用于指针通常是一个错误,要得到一个指针指向之对象的type_info,应当先将其解引用(例如:"typeid(*p);")。

一般地讲,能用虚函数解决的问题就不要用 "dynamic_cast",能够用 "dynamic_cast" 解决的就不要用 "typeid"。比如:



void
rotate(
INconstCShape&iS)
{
   
if (typeid(iS) == typeid(CCircle))
    {
       
// ...
    }
   
elseif (typeid(iS) == typeid(CTriangle))
    {
       
// ...
    }
   
elseif (typeid(iS) == typeid(CSqucre))
    {
       
// ...
    }

   
// ...
}

以上代码用 "dynamic_cast" 写会稍好一点,当然最好的方式还是在CShape里定义名为 "rotate" 的虚函数。

虚函数是C++众多运行时多态特性中开销最小,也最常用的机制。虚函数的好处和作用这里不再多说,应当注意在对性能有苛刻要求的场合,或者需要频繁调用,对性能影响较大的地方(比如每秒钟要调用成千上万次,而自身内容又很简单的事件处理函数)要慎用虚函数。

需要特别说明的一点是:虚函数的调用开销与通过函数指针的间接函数调用(例如:经典C程序中常见的,通过指向结构中的一个函数指针成员调用;以及调用DLL/SO中的函数等常见情况)是相当的。比起函数调用本身的开销(保存现场->传递参数->传递返回值->恢复现场)来说,一次指针间接引用是微不足道的。这就使得在绝大部分可以使用函数的场合中都能够负担得起虚方法的些微额外开销。

作为一种支持多继承的面向对象语言,虚基类有时是保证类层次结构正确一致的一种必不可少的手段。但在需要频繁使用基类提供的服务,又对性能要求较高的场合,应该尽量避免使用它。在基类中没有数据成员的场合,也可以解除使用虚基类。例如,在上图中,如果类 "BB" 中不存在数据成员,那么 "BB" 就可以作为一个普通基类分别被 "B1" 和 "B2" 继承。这样的优化在达到相同效果的前提下,解除了虚基类引起的开销。不过这种优化也会带来一些问题:从 "DD" 向上强制到 "BB" 时会引起歧义,破坏了类层次结构的逻辑关系。

上述特性的空间开销一般都是可以接受的,当然也存在一些特例,比如:在存储布局需要和传统C结构兼容的场合、在考虑对齐的场合、在需要为一个本来尺寸很小的类同时实例化许多对象的场合等等。

0 0
原创粉丝点击