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(),这两个函数都必须在每个新派生类中重新定义:
class security {
protected:
enum { baseID = 1000; }
public:
virtual int dynamic_type(int ID) {
if (ID==baseID) return 1;
return 0;
}
}
class stock : public security {
protected:
enum { typeID = baseID + 1 };
public:
int dynamic_type(int ID) {
if (ID==typeID) return 1; // 如果typeID相等,毫无疑问是同一个类的指针
return security::dynamic_type(ID); // 如果不等,还要传递给它的父类,父类再传给祖父类。。。
}
static stock* dynacast(security* s) { // 把自己的typeID传给外部指针所包含的dynamic_type()函数
if (s->dynamic_type(typeID)) // 这种方法好,每个类都提供dynamic_type(),而不是提供自身typeID的值
return (stock*)s; // 每个子类应该提供的指针类型是不同的,所以必须在每一个类中重定义dynacast()
return 0;
}
}
总结:每个子类都必须创建它自己的typeID。baseID可以从派生类中直接访问,这一点是关键所在,是需要protected成员的典型事例。因为enum必须在编译的时候计算出值的大小。
C++的动态映射运算符比上面的例子要多一项功能:它可以比较两个继承层次中的类型,这两个继承层次可以是完全分开的。这增加了系统的通用性,使它适用于跨层次体系的类型比较。
=========================总结===========================
情况1:有时候我们可能发现基类并没有我们想要的虚函数(基类来自类库或其他来源),就可以用RTTI作为一种解决办法:
我们可以继承一个新类并加上我们的成员函数。在代码的其它地方可以检测到这个新增类,并调用那个成员函数。这不会破坏多态性和程序逻辑的可扩展性,因为新增一个类不需要我们使用switch语句。
我的理解:有了系统提供的RTTI,随时判断这个类是不是新增类,因为基类没有想要的功能,只有新增类才能使用新功能。并且对原来的类库没有任何影响。
情况2:把一个特征放在基类中,可能意味着为了某个特定类的利益,所有从该类派生出的类都保留了一些无意义的虚函数的残留,这是的接口变得不清晰。
我的理解:RTTI可以使得类库层次的功能设计更合理。不应该为了照顾少数类而拖累大家。
最后的总结:一般都是把一个指针向上映射为一个基类指针,然后使用基类的接口(通过虚函数),但偶尔需要知道一个基类指针指向的对象的确切类型来提高程序的效率,这时候就可以使用RTTI。这是一个非常有用的特征,它的好处是:
1. 不需要把它建立在其它类库中
2. 在继承的过程中,不需要额外的编程来管理RTTI配置,并且语法是一致的。
缺点:不理解虚函数的程序员,会用RTTI去做类型检查,类似于switch功能。但这是不对的,应该尽量使用多态功能。
=========================其它细节===========================
------------------------------typeid的特殊用法-----------------------------------
这时候dynamic_cast与typeid()之间产生了有趣的差异:typeid()总是产生一个typeinfo对象的引用来描述一个对象的准确类型,因此它不会给出中坚层次的信息。
typeid(D2) != typeid(mi2*); // true
typeid(D2) == typeid(d2*); // true
其它例子:
typeid(47) == typeid(int)
int i;
typeid(i) == typeid(int)
typeid(&i) == typeid(int*)
表达式也可以用typeid()运算符,因为它们也有一个类型:
typeid(r.f()) == typeid(float)
此外,typeid可以用于非多态类型(基类中没有虚函数),但结果可能不是我们想要的。所以一般RTTI用于多态类。此外,typeid()也不能用于空指针的引用。
typeid(*s) == typeid(Shape)
typeid(*s) == typeid(Circle)
指针与引用存在明显不同,因为引用总是由编译器逆向引用,而指针的类型或者它指向的类型可能要检测:
class B {}
class D: public B {}
B* p = new D;
B& r = *p;
typeid(p) == typeid(B*) // 这是比较指针,只看指针类型,不看它所指向的对象类型
typeid(p) != typeid(D*) // 同上
typeid(r) == typeid(D) // 这是比较对象,r只看指向的对象,不看它的基类型
typeid(*p) == typeid(D) // 这是比较对象,所以看实际对象
typeid(*p) != typeid(B) // 同上
typeid(&r) == typeid(B*) // 这是比较指针,所以看定义时候的指针类型
typeid(&r) != typeid(D*) // 同上
总结:
对于指针,typeid()看的是(定义时的)基类,而不是派生类
对于引用,typeid()看的是派生类,而不是基类
------------------------------两种Bad-cast-----------------------------------
1. dynamic_cast转换一个完全不相关的类
2. typeid操作一个空指针
------------------------------其它语法-----------------------------------
static_cast : 为了“行为良好”和“行为较好”而使用的映射,包括一些我们可能现在不用的映射(如向上映射和自动类型转换)。
const_cast :用于映射常量和变量(const和volatile)
reinterpret_cast:为了映射到一个完全不同的意思。这是所有映射中最危险的。