C++_新特性2-RTTI运行阶段类型识别

C++_新特性2-RTTI运行阶段类型识别

这部分属于C++的新特性,感觉比较高阶的特性。我把它归于属于奇技淫巧的范畴。了解即可。

RTTI运行阶段类型识别(Runtime Type Identification)的简称。

这是添加到C++中的新特性。

很多老式的编译器不支持它,或者可能包含开关RTTI的编译器设置。

RTTI旨在位程序在运行阶段确定对象的类型提供一种标准方式

很多类库已经为其对象提供了实现这种功能的方式,但是由于C++内部不支持,因此各个厂商的机制通常互不兼容。

创建一种RTTI语言标准将使得未来的库能够彼此兼容

一、RTTI的用途

    假设有一个类层次结构,其中的类都是从同一个基类派生而来的,则可以让基类指针指向其中任何一个类的对象。

这样便可以调用这样的函数:在处理一些信息后,选择一个类,并创建这种类型的对象,然后返回它的地址,而该地址可以被赋值给基类的指针。

但是如何知道指针指向的是哪种对象呢

    在回答这个问题之前,首先要考虑为何要知道对象类型。可能希望调用类方法的正确版本,在这种情况下,只要该函数是类层次结构中所有成员都拥有的虚函数,并不真正需要知道对象的类型。

但派生对象可能包含而不是继承而来的方法,在这种情况下,只有某些类型的对象可以使用该方法。也可能是出于调试目的,想跟踪生成的对象的类型。

二、RTTI的工作原理

C++有3个支持RTTI的元素

  dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针。否则,该运算符返回0-空指针。

  typeid运算符返回一个而支出对象的类型的值。

  type_info结构存储了有关特定类型的信息。

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

RTTI只适用于包含虚函数的类。

下面详细介绍RTTI的这3个元素:

1、dynamic_cast运算符

     这个运算符不能够回答“指针指向的是哪类对象”这样的问题。但是能够回答“是否可以安全地将对象的地址赋给特定类型的指针”这样的问题。我们来看一下这意味着什么?

假设有下面这样的类层次结构:

  class Grand { }

  class Superb { }

  class Magnificent { }

接下来假设有下面的指针:
Grand * pg = new Grand;

Grand * ps = new Superb;

Grand * pm = new Magnificent;

最后,对于下面的类型转换:

Magnificent * p1 = (Mginificent *) pm;      //#1

Magnificent * p2 = (Magnificent *) pg;      //#2

Superb * p3 = (Magnificent *) pm;           //#3

#1 是安全的,因为对象是Magnificent类型,而指针也是Magnificent派生类类型;

#2 是不安全的,因为对象是Grand基类类型,而指针是Magnificent派生类类型;

#3 是安全的,因为它将派生类Magnificent对象赋值给直接基类Superb类型的指针;

对于派生类对象Magnifcent将其地址赋值给三种类型的指针都是安全的。

虚函数确保了这3种指针中的任何一种指向Magnificent对象时,都将调用Magnificent方法。

这里要注意一下,与问题“指针指向的是哪种类型的对象”相比,问题“类型转换是否安全”更加通用,也更有用。

通常想知道类型的原因在于:知道类型后,就可以知道调用特定的方法是否安全。

要调用方法,类型不一定要完全匹配,而可以是定义了方法的虚拟版本的基类类型。

接下来看一个使用dynamic_cast例子:

Superb * pm = dynamic_cast<Superb *>(pg);

其中pg指向一个对象,指针pg的类型是否可被安全地转换为Superb *?如果可以,运算符将返回对象的地址,否则,返回一个空指针。

  如果指向的对象(*pt)的类型为Type或者是从Type直接或间接派生而来的类型, 则下面的表达式将指针pt转换为Type类型的指针:

    dynamic_cast<Type *>(pt)

  否则,结果为0,返回空指针。 

 1 //rtti1.cpp  -- using the RTTI dyanmic_cast operator
 2 #include <iostream>
 3 #include <cstdlib>
 4 #include <ctime>
 5 
 6 using std::cout;
 7 class Grand
 8 {
 9 private:
10     int hold;
11 public:
12     Grand(int h =0): hold(h) {}
13     virtual void Speak() const {cout<<"I am a grand class!
";}
14     virtual int Value() const {return hold;}
15 };
16 
17 class Superb : public Grand
18 {
19 public:
20     Superb(int h =0):Grand(h) {}
21     void Speak() const {cout<<"I am a superb class!
";}
22     virtual void Say() const
23         {cout<<"I hod the superb value of"<<Value()<<"!
";}    
24 };
25 
26 class Magnificent : public Superb
27 {
28 private:
29     char ch;
30 public:
31     Magnificent(int h = 0, char c = 'A'):Superb(h),ch(c) {}
32     void Speak() const {cout<<"I am a magnificent class!!!
";}
33     void Say() const {cout<<" I hold the character "<<ch<<" and the integer"<<Value()<<"!
";}  
34 };
35 
36 Grand * GetOne();
37 
38 int main()
39 {
40     std::srand(std::time(0));
41     Grand * pg;
42     Superb * ps;
43     for (int i = 0; i<5; i++)
44     {
45         pg = GetOne();
46         pg->Speak();
47         if (ps = dynamic_cast<Superb *>(pg))
48             ps->Say();
49     }
50     return 0;
51 }
52 
53 Grand * GetOne()
54 {
55     Grand * p;
56     switch(std::rand() %3)
57     {
58         case 0: p = new Grand(std::rand() % 100);
59                     break;
60         case 1: p = new Superb(std::rand() % 100);
61                     break;
62         case 2: p = new Magnificent(std::rand() % 100, 'A'+std::rand()%26);
63                     break;
64     }
65     return p;
66 }
67     

程序说明

顶一个一个GetOne()函数,该函数随机创建这3种类中某种类的对象,并对其进行初始化;然后将地址作为Grand * 指针返回。

循环将该指针赋给Grand * 变量 pg,然后用pg调用Speak()函数。因为这个函数是虚拟的,所以代码能够正确地调用指向的对象的Speak()版本。

但是不能用相同的方式来调用Say()函数,因为Grand类没有定义它。但是可以使用dynamic_cast运算符来检查是否可以将pg的类型安全转换为Superb指针。

如果对象的类型为Superb或Magnificent的话,则可以安全转换。这样就可以安全地调用Say()函数。

2、typeid运算符和type_info类

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

类名;

结果为对象的表达式;

typeid运算符返回一个队type_info对象的引用,其中,type_info是在头文件typeinfo中定义的一个类。

type_info类重载了 == 和 != 运算符,以便可以使用这些运算符来对类型进行比较。

例如,如果pg指向的是一个Magnificent对象。则下述表达式的结果为true或者为false。

  typeid(Magnificent) == typeif(*pg)

如果*pg是一个空指针,程序将引发bad_typeid的异常。该异常类型是从exception类派生而来的,是在头文件typeinfo中声明的。

 

type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串,通常但并非一定是类的名称。

 

接下来程序对上段程序做了一些修改,以使用typeid运算符和name()成员的函数。注意,它们都适用于dynamic_cast和virtual函数不能处理的情况。

typeid测试用来选择一种操作,因为操作不是类的方法,所以不能通过类指针调用它。name()方法语句演示了如何将方法用于调试。

 1 //rtti2.cpp -- using dynamic_cast, typeid, and type_info
 2 #include <iostream>
 3 #include <cstdlib>
 4 #include <ctime>
 5 #include <typeinfo>
 6 using namespace std;
 7 
 8 class Grand
 9 {
10 private:
11     int hold;
12 public:
13     Grand(int h = 0):hold(h) {}
14     virtual void Speak() const {cout<<"I am a grand class!
";}
15     virtual int Value() const {return hold;}
16 };
17 
18 
19 class Superb:public Grand
20 {
21 public:
22     Superb(int h = 0):Grand(h)  {}
23     void Speak() const {cout<<"I am a superb class!!
";}
24     virtual void Say() const
25         {cout<<"I hold the superb value of "<<Value()<<"!
";}
26 };
27 
28 
29 class Magnificent : public Superb
30 {
31 private:
32     char ch;
33 public:
34     Magnificent(int h =0, char cv = 'A'):Superb(h),ch(cv) {}
35     void Speak() const {cout << "I am a magnificent class!!
"}
36     virtual void Say() const
37         {coust << "I hold ther character "<<ch<<"and the integer"<<Value()<<"!
";}
38 };
39 
40 Grand * GetOne();
41 int main()
42 {
43     srand(time(0));
44     Grand * pg;
45     Superb * ps;
46     for(int i = 0; i<5; i++)
47     {
48         pg = GetOne();
49         cout<<"Now processing type "<<typeid(*pg).name()<<".
";
50         pg ->Speak();
51         if (ps = dynamic_cast<Superb *>(pg))
52             ps ->Say();
53         if(typeid(Magnificent)==typeid(*pg))
54             cout<<"Yes, you're really magnificent.
";
55     }
56     return 0;
57 }
58 
59 Grand * GetOne()
60 {
61     Grand * p;
62     switch (rand() % 3)
63     {
64         case 0 :  p = new Grand(rand() %100);
65                             break;
66         case 1 :  p = new Superb(rand() %100);
67                             break;
68         case 2 :  p = new Magnificent(rand() %100, 'A'+rand())%26);
69                             break;
70     }
71     return p;
72 }

3、误用RTTI的例子

   接下来讨论,对RTTI应避免的编程方式:

Grand * pg;

Superb * ps;

for(int i =0; i<5; i++)

{

  pg = GetOne(0;

  pg->Speak();

  if(ps = dynamic_cast<Superb *>(pg))

    ps->Say();

}

通过放弃dynamic_cast和虚函数,而使用typeid,可以将上述代码重新编写为:

Grand * pg;

Superb * ps;

Magnificent * pm;

for(int i = 0; i<5; i++)

{

    pg = Getone();

    if ( typeid(Magnificent) == typeid(*pg))

    {

        pm = (Magnificent *) pg;

        pm -> Speak();

        pm ->Say();

    }

    else if (typeid(Superb) == typeid(*pg))

    {

        ps = (Superb *)pg;

        ps -> Speak();

        ps -> Say();

    }

    else

        pg->Speak();

}

上述代码不仅比原来的更难看,更长,而且显式地指定各个类存在严重的缺陷。

例如,假设你发现必须从Magnificent类派生出一个Insufferable类,而后者需要重新定义Speak()和Say()。

用typeid来显式地测试每种类型时,就需要修改for循环的代码,添加一个else if,但无需修改原来的版本。

下面的语句适合所有从Grand派生而来的类:

pg -> Speak();

而下面的语句适用于所有从Superb派生而来的类:

if (ps = dynamic_cast<Superb *>(pg))

  ps ->Say();

如果发现在扩展的if else语句系列中使用了typeid,则应考虑是否应该使用虚函数和dynamic_cast。