C++编程思想,第十八章《RTTI运行时类型识别》

C++编程思想,第十八章《RTTI运行时类型识别》

(加了一些少量自己的话,应该不会误导大家,另外翻译版本比较罗嗦,把做了适当精简)

RTTI是我们只有一个指向基类的指针或者引用的时候,确定一个对象的准确类型。这是C++的第二大特征。编程时候遇到了特殊的问题,而我们只要知道了一个一般指针的准确类型它就会迎刃而解。C++的意图是:尽可能的使用虚函数,必要的时候才使用RTTI。因为使用RTTI在维护阶段会丢失多态性的非常重要的价值。有了RTTI以后,就不用再类层次里建立类信息。

面向对象的关键特征是使用多态,多态的关键在于使用基类指针指向子类对象。一般情况下,我们不需要知道一个类的确切类型,虚函数机制可以实现那种类型的正确行为。但有的时候,我们有指向某个对象的基类指针,确定该对象的准确类型是很有用的。这些信息让我们更高效地完成一个特定情况下的操作,防止基类的接口变得很笨拙。所以大多数类库用一些虚函数来提供运行时的类型信息。

例如:当异常处理功能加入到C++的时候,它要求知道有关这个对象的准确类型信息。

许多函数库把虚函数放在基类中,使运行时返回特定对象的类型信息。比如 isA(), typeOf(),instanceOf,这些就是开发商定义的RTTI函数。RTTI与异常一样,依赖驻留在虚函数表中的类型信息。如果试图在一个没有虚函数的类上使用RTTI,就得不到预期的结果。

RTTI的两种使用方法:
1. 第一种是typeid(),它很像sizeof,看上去像一个函数,但实际上它是由编译器实现的。
typeid()带有一个参数,它可以是一个对象引用或者指针,返回全局typeinfo类的常量对象的一个引用。并使用==或者!=来互相比较这些对象。
可以使用name()来获得类型的名称。例如:cout << typeid(*s).name();
可以使用before(typeinfo&)来查询一个typeinfo对象是否在另一个typeinfo对象的前面(按照继承顺序),例如 if(typeid(me).before(typeid(you))) //...

2. 第二种是“dynamic_cast安全类型向下映射”(这种方法用的更多)
可以使用C++的静态映射static_cast强制执行,但这样做很危险,因为没有办法明确地知道它实际上是什么。
所以使用 shape* sp = new circle;
circle* cp = dynamic_cast<circle*>(sp);
if (cp) cout << "cas successful"; // 如果返回一个地址,成功;返回null,说明它不是一个circle* 对象。

以一个例子为例,判断 circle, ellipse, rectange分别由多少个,既可以使用静态数据成员方法(通常用于累计数据,或者判断地址),也可以使用动态映射方法。但是静态数据成员方法只能用于我们拥有源代码并安装了静态数据成员和成员函数时,或者开发商已经提供了这些数据和函数,所以有局限。

动态映射不仅可以用来确定准确的类型,也可用于多层次继承关系中的中间类型。多重继承的时候,如果将一个实际指向子类的基类指针做强制转换成子类指针也会出错。
class d1 {} // 不要忘了内部一定要含有 virtual 函数
class d2 {}
class mi: public d1, public d2 {};
class mi2: public mi {};

d2* D2 = new mi2; // 所以D2 同时是d1,d2,mi,mi2
mi2* MI2 = dynamic_cast<mi2*>(D2); // 可以得到中间层次类的指针
mi* MI = dynamic_cast<mi*>(D2); // 可以得到中间层次类的指针

甚至可以从一个根类型映射到另一个:
d1* D1 = dynamic_cast<d1*>(D2);

=========================RTTI的实现原理===========================

典型的RTTI是通过在VTABLE中放一个额外的指针来实现的。这个指针指向一个描述该特定类型的typeinfo结果(每个新类只产生一个typeinfo的实例)。所以typeid()表达式的作用实际上很简单。VPTR用来取typeinfo的指针,然后产生一个结果typeinfo结构的一个引用——这是一个简单的决定性步骤——我们知道它要花多少时间。

对于dynamic_cast<目标*><源指针>,多数情况下下很容易。分别取出源指针和目标指针的RTTI信息,然后调用库中的一个例程判断两者相同,或者是目标指针类型的基类。多重继承的时候,情况会复杂一些。

动态映射的类库必须检查一串长长的基类列表,所以动态映射的开销要比typeid()大,当然我们得到的信息也不同,这对于我们的问题来说很关键。并且这是非确定性的。因为查找一个基类要比查找一个派生类花更多的时间。此外,动态映射允许比较任何类型,不限于在同一继承层次中比较两个类。

个人猜测,VC 6.0 的RTTI开关之所以是默认关闭的,因为RTTI会对每一个类加入更多的信息,比如name()函数,对于typeid也要提供额外信息(要#include <typeinfo.h>),增大了开销。

自己创建RTTI:
从本质上说,RTTI只要两个函数就行了。一个用来指明类的准确类型的虚函数,一个取得基类的指针并将它向下映射成派生类(可能也会处理引用)。有许多方法来实现我们自己的RTTI,但都要求每个类有一个唯一的标识符和一个能产生类型信息的虚函数。下面的例子定义了dynacast()的静态成员函数,它调用一个类型信息函数dynamic_type(),这两个函数都必须在每个新派生类中重新定义:

发表回复