什么是 RTTI

RTTI(Runtime Type Indentification) 即运行阶段类型识别。 这是 C++新引进的特性之一。RTTI旨在为程序在运行阶段确定对象的类型提供一种标准方式。

RTTI 用途

考虑一种情况:假如现在有一个基类 Base,并且基类中包含有虚函数 Fun1,然后派生类 A 继承于基类 Base,并实现基类的虚函数Fun1,同时,派生类 A 又定义一个虚函数 Fun2,接着,派生类 B 继承于 A,并且实现了基类中的两个虚函数 Fun1和 Fun2,那么,当我们选择一个类 A 或 B,创建一个对象,并将指针赋值给基类指针,这时候,当我们要用改指针的时候就不知道该指针真正指向哪个对象。
那么问题来了,可能你会问,干嘛要知道该指针类型呢?如果说通过该指针去调用基类本身就有的虚函数,则可以正常调用,不需要知道对象的类型,但是当调用派生类中新定义的函数,这时候就会出错啦。这种情况下,只有某些类型的对象可以使用该方法。所以就需要进行判断对象类型,或者进行相应的强制合理转换对象类型。

光是描述可能太难理解,现在咱来看看实际的例子:

#include <iostream>using namespace std;class Base
{
public:virtual void Fun1(){}virtual ~Base();
};class A : public Base
{
public:virtual void Fun1(){}virtual void Fun2(){}virtual ~A();
};class B :public A
{
public:void Fun1(){}void Fun2(){}virtual ~B();
};int main()
{Base * p1;Base * p2;Base * p3;p1 = new Base();p2 = new A();p3 = new B();p2->Fun1();//right
//    p2->Fun2();//error..p3->Fun1();//right
//    p3->Fun2();//error..return 0;
}Base::~Base(){}A::~A(){}B::~B(){}

上例子可以看到,p2->Fun2()以及p3->Fun2()是会报错的,因 Fun2()是在 A 类中定义的,而 p2指针是赋给基类 Base 的,Base 中并没有 Fun2()函数。

对于这种问题,RTTI 提供了解决方案。

C++中三个支持 RTTI 的元素

  • dynamic_cast :改运算符将使用一个指向基类的指针来生成一个指向派生类的指针。如果转换不合理,会转换失败,并返回0—空指针。
  • typeid:运算符返回一个指出对象的类型的值。
  • type_info:存储了有关特定类型的信息。

注意:RTTI 只适用于包含虚函数的类。只能讲 RTTI 用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应将派生对象的地址赋给基类指针。

dynamic_cast

dynamic_cast运算符是最常见的 RTTI 组件,它可以安全地将对象的地址赋给特定类型的指针。

还是根据以上示例,咱们来看以下几个转换是否安全:

Base * pBase = new Base();
A * pA  = new A();
B * pB = new B();B * p1 = (B*)pB; 		//#1
B * p2 = (B*)pBase; 		//#2
A * p3 = (B*)pB; 		//#3

上面示例中#2是不安全的,因为只有那些指针类型与对象的类型(或对象的直接或间接基类的类型)相同的类型转换才一定是安全的。#2的转换中,希望将基类指针转换成派生类指针,但是由于基类指针 Base 中并不具有 B 类中的 Fun2()函数,所以这个转换就是不安全的。而#1和#3可以安全转换。

那么,我们怎么来检查这些转换是否安全呢?这时候dynamic_cast就排上用场了。
看一个通用的转换示例:

dynamic_cast<Type *>(pt)

如果只想的对象(*pt)的类型为 Type 或者从 Type 直接或间接派生而来的类型,则以上表达式将指针 pt 转换成 Type 类型的指针,否则,转换失败,返回空指针0.

也就是说dynamic_cast会在转换的时候动态的进行类型安全检查,如果该转换安全,那么就正常转换,如果不安全,就返回空指针。

我们在将上面的#2表达式修改如下:

B * p2 = dynamic_cast<B*>(pBase);

这个转换不安全,所以p2的值为空。

OK,接下来看一个正式的例子:

#include <iostream>
#include <cstdlib>
#include <ctime>using namespace std;class Grand
{
public:Grand(int h = 0):hold(h){}virtual void Speak() const {cout << "I'm a grand class! n";}virtual int Value() const{return  hold;}virtual ~Grand();
private:int hold;
};class Superb : public Grand
{
public:Superb(int h = 0):Grand(h){}void Speak() const {cout << "I'm a superb class n";}virtual void Say() const{cout << "I hold the superb value of " << Value() << "!n";}virtual ~Superb();};class Magnificent: public Superb
{
public:Magnificent(int h = 0,char c = 'A'): Superb(h), ch(c){}void Speak() const {cout << "I'm a magnificent class !!n";}void Say() const {cout << "I hold the character" << ch<< "and the integer " << Value() << "!n";}private:char ch;
};Grand * GetOne()
{Grand * p = nullptr;switch(std::rand() %3){case 0: p = new Grand(std::rand() % 100);break;case 1: p = new Superb(std::rand() % 100);break;case 2: p = new Magnificent(std::rand() % 100,'A' + std::rand() % 26);break;}return  p;
}int main()
{std::srand(std::time(0));Grand * pg;Superb * ps;for(int i = 0 ; i < 5 ; i++){pg = GetOne();pg->Speak();if(ps = dynamic_cast<Superb*>(pg)){ps->Say();}}return 0;
}Grand::~Grand(){}Superb::~Superb(){}

输出:

I'm a grand class! 
I'm a superb class 
I hold the superb value of 64!
I'm a magnificent class !!
I hold the characterUand the integer 71!
I'm a grand class! 
I'm a magnificent class !!
I hold the characterCand the integer 51!

这个示例中,GetOne()方法随机创建一个指针,并赋值给基类Grand指针,在 main 函数中pg->Speak();可以正常调用,是因为 Speak()方法本身就是在基类中定义的,而Say()方法是在派生类中定义的,基类指针无法调用,所以这里加入了动态转换 dynamic_cast<Superb*>(pg),如果转换合法,将会正常调用 Say()方法,如果不合法,则返回空指针。

所以根据输出结果我们看到,如果随机创建出来的是基类指针,那么不会调用 Say()方法。

typeid运算符和 type_info类

typeid运算符使得能够确定两个对象是否为同种类型。它可以接受两种参数:

  • 1.类名
  • 2.结果为对象的表达式

typeid 运算符返回一个对type_info对象的引用,其中,type_info是在头文件 typeinfo 中定义的一个类。type_info类重载了==和 != 运算符,以便可以使用这些运算符来对类型进行比较。

OK,接下来将上面的示例修改一下:

int main()
{std::srand(std::time(0));Grand * pg;Superb * ps;for(int i = 0 ; i < 5 ; i++){pg = GetOne();pg->Speak();if(ps = dynamic_cast<Superb*>(pg)){ps->Say();}//类型判断if(typeid (Magnificent) == typeid (*pg)){cout<< "Yes, you are really Manificent.n";}}return 0;
}

运行结果:

I'm a grand class! 
I'm a magnificent class !!
I hold the characterBand the integer 32!
Yes, you are really Manificent.
I'm a grand class! 
I'm a magnificent class !!
I hold the characterGand the integer 95!
Yes, you are really Manificent.
I'm a grand class! 

上述代码中,typeid (Magnificent) == typeid (*pg),如果 pg 指向的是Magnificent对象,则该表达式的结果为 true,否则为 false。如果 pg 是一个空指针,程序将引发 bad_typeid异常。该异常类型是从 exception 类派生而来,是在头文件 typeinfo 中声明的。
type_info类的实现随编译器厂商而异,但包含一个 name()成员,该函数返回一个随实现而异的字符串:通常(但并非一定)是类 的名称。
为了查看效果,我们继续在上述代码中添加打印输出:

int main()
{std::srand(std::time(0));Grand * pg;Superb * ps;for(int i = 0 ; i < 5 ; i++){pg = GetOne();pg->Speak();if(ps = dynamic_cast<Superb*>(pg)){ps->Say();}//输出类型名cout<< "typeid (*pg) = " << typeid (*pg).name() << endl;if(typeid (Magnificent) == typeid (*pg)){cout<< "Yes, you are really Manificent.n";}}return 0;
}

结果:

I'm a magnificent class !!
I hold the characterKand the integer 59!
typeid (*pg) = 11Magnificent
Yes, you are really Manificent.
I'm a grand class! 
typeid (*pg) = 5Grand
I'm a grand class! 
typeid (*pg) = 5Grand
I'm a superb class 
I hold the superb value of 36!
typeid (*pg) = 6Superb
I'm a magnificent class !!
I hold the characterOand the integer 49!
typeid (*pg) = 11Magnificent
Yes, you are really Manificent.