[转]c++中RTTI的观念和使用

下面这篇文章虽然有点老,但对C++的RTTI基本原理讲的比较透彻。
该文章摘自UMLCHINA网站,是台湾一个群体写的,我根据大家比较熟悉的方式,修改了一些名词的说法,如衍生(派生)等,让大家可以方便的阅读。
C++的 RTTI 观念和用途
物泽C++应用小组
  自从1993年Bjarne Stroustrup 〔注1 〕提出有关C++ 的RTTI功能之建议�o以及C++
的异常处理(exception handling)需要RTTI�r最近新推出的C++ 或多或少已提供RTTI。然而,若不小心使用RTTI�o可能会导致软件弹性的降低。本文将介绍RTTI的观念和近况�o并说明如何善用它。
什么是RTTI�t
  在C++ 环境中�o头文件(header file) 含有类之定义(class definition)亦即包含有
关类的结构资料(representational information)。但是�o这些资料只供编译器(compi
ler)使用�o编译完毕后并未留下来�o所以在执行时期(at run-time) �o无法得知对象的
类资料�o包括类名称、数据成员名称与类型、函数名称与类型等等。例如�o两个类�o其继承关系如下图:
若有如下指令�s
                      Figure *p;
                      p = new Circle();
                    Figure &q = *p;
在执行时�op 指向一个对象�o但欲得知此对象之类资料�o就有困难了。同样欲得知q 所参考(reference) 对象的类资料�o也无法得到。
  RTTI(Run-Time Type Identification)就是要解决这困难�o也就是在执行时�o您想知
道指针所指到或参考到的对象类型时�o该对象有能力来告诉您。
  随着应用场合之不同�o所需支持的RTTI范围也不同。最单纯的RTTI包括�s
●类识别(class identification)──包括类名称或ID。
●继承关系(inheritance relationship)──支持执行时期的「往下变换类型」(downw
ard casting)�o亦即动态变换类型(dynamic casting) 。
在对象数据库存取上�o还需要下述RTTI�s
●对象结构(object layout) ──包括属性的类型、名称及其位置(position或offset
)。
●成员函数表(table of functions)──包括函数的类型、名称、及其参数类型等。
其目的是协助对象的I/O 和持久化(persistence) �o也提供调试讯息等。
      若依照Bjarne Stroustrup 之建议〔注1 〕�oC++ 还应包括更完整的RTTI�s
●能得知类所实例化的各对象 。
●能参考到函数的源代码。
●能取得类的有关在线说明(on-line documentation) 。
其实这些都是C++ 编译完成时�o所丢弃的资料�o如今只是希望寻找个途径来将之保留到执行期间。然而�o要提供完整的RTTI�o将会大幅提高C++ 的复杂度�u
RTTI可能伴随的副作用
  RTTI最主要的副作用是�s程序员可能会利用RTTI来支持其「复选」(multiple-select
ion)方法�o而不使用虚函数(virtual function)方法。
  虽然这两种方法皆能达到多态化(polymorphism) �o但使用复选方法�o常导致违反著名的「开放�u封闭原则」(open/closed principle) 〔注2 〕。反之�o使用虚函数方法则可合乎这个原则,  请看下图�s
        Circle和Square皆是由Figure所派生出来的子类�o它们各有自己的draw()函数。当
C++ 提供了RTTI�o就可写个函数如下�s
  void drawing( Figure *p )
          {
                    if( typeid(*p).name() == "Circle" )
                                          ((Circle*)p)  ->  draw();
                    if( typeid(*p).name() == "Rectangle" )
                                          ((Rectangle*)p) -> draw();
            }
  虽然drawing() 函数也具有多型性�o但它与Figure类体系的结构具有紧密的相关性。
当Figure类体系再派生出子类时�odrawing() 函数的内容必须多加个if指令。因而违反
了「开放�u封闭原则」�o如下�s
很显然地�odrawing() 函数应加以修正。
          想一想�o如果C++ 并未提供RTTI�o则程序员毫无选择必须使用虚函数来支持drawing() 函数的多型性。于是程序员将draw()宣告为虚函数�o并写drawing() 如下�s
        void drawing(Figure *p)
                                {        p->draw();          }
        如此�oFigure类体系能随时派生类�o而不必修正drawing() 函数。亦即�oFigure体
系有个稳定的接口(interface) �odrawing() 使用这接口�o使得drawing() 函数也稳定
�o不会随Figure类体系的扩充而变动。这是封闭的一面。而这稳定的接口并未限制Figure体系的成长�o这是开放的一面。因而合乎「开放�u封闭」原则�o软件的结构会更具弹性�o更易于随环境而不断成长。
RTTI的常见的        使用场合
          一般而言�oRTTI的常见使用场合有四�s异常处理(exceptions handling)、动态转
类型(dynamic casting) 、模块集成、以及对象I/O 。
1.异常处理──  大家所熟悉的C++ 新功能�s异常处理�o其需要RTTI�o如类名称等。
2.动态转类型──  在类体系(class hierarchy) 中�o往下的类型转换需要类继承的RT
TI。
3.模块集成──  当某个程序模块里的对象欲跟另一程序模块的对象沟通时�o应如何得知对方的身分呢�t知道其身分资料�o才能呼叫其函数。一般的C++ 程序�o常见的解决方法是──在源代码中把对方对象之类定义(即存在头文件里)包含进来�o在编译时进行连结工作。然而�o像目前流行的主从(Client-Server) 架构中�o客户端(client)的模块对象�o常需与主机端(server)的现成模块对象沟通�o它们必须在执行时沟通�o但又常无法一再重新编译。于是靠标头文件来提供的类定义资料�o无助于执行时的沟通工作�o只得依赖RTTI了。
4.对象I/O ──  C++ 程序常将其对象存入数据库�o未来可再读取之。对象常内含其它
小对象�o因之在存入数据库时�o除了必须知道对象所属的类名称�o也必须知道各内含小对象之所属类�o才能完整地将对象存进去。储存时�o也将这些RTTI资料连同对象内容一起存入数据库中。未来�o读取对象时�o可依据这些RTTI资料来分配内存空间给对象。
RTTI从那里来�t
  上述谈到RTTI的用途�o以及其副作用。这众多争论�o使得RTTI的标准迟迟未呈现出来。也导致各C++ 开发环境提供者�o依其环境所需而以各种方式来支持RTTI�o且其支持RTTI的范围也所不同。  目前常见的支持方式包括�s
●由类库提供RTTI──例如�oMicrosoft 公司的Visual C++环境。
●由C++ 编译器(compiler)提供──例如�oBorland C++ 4.5 版本。
●由源代码产生器(code generator)提供──例如Bellvobr系统。
●由OO数据库的特殊预处理器(preprocessor)提供──例如Poet系统。
●由程序员自己加上去。
这些方法皆只提供简单的RTTI�o其仅为Stroustrup先生所建议RTTI内涵的部分集合而已。相信不久的将来�o会由C++ 编译器来提供ANSI标准的RTTI�o但何时会订出这标准呢�t没人晓得吧�u
程序员自己提供的RTTI
  通常程序员自己可提供简单的RTTI�o例如提供类的名称或识别(TypeID)。最常见的方法是�s为类体系定义些虚函数如Type_na() 及Isa() 函数等。请先看个例子�s
  class Figure  { };
  class Rectangle : public Figure    { };
  class Square : public Rectangle
              {      int data;
                      public:
                          Square() { data=88; }
                          void Display()  { cout << data << endl; }
                  };
  void main()
            {    Figure *f = new Rectangle();
                      Square *s = (Square *)f;
                      s -> Display();
            }
这时s 指向Rectangle 之对象�o而s->Display()呼叫Square::Display() �o将找不到da
ta值。若在执行时能利用RTTI来检查之�o就可发出错误讯息。于是�o自行加入RTTI功能
�s
  class Figure
          {  public:
                        virtual char* Type_na()
                                                    {  return "Figure";  }
                          virtual int Isa(char* cna)
                              {  return !strcmp(cna, "Figure")? 1:0;  }
          };
  class Rectangle:public Figure
        {    public:
                          virtual char* Type_na()
                                        {  return "Rectangle";  }
                          virtual int Isa(char* cna)
                                {  return !strcmp(cna, "Rectangle")?
                                                    1 : Figure::Isa(cna);
                                }
              static Rectangle* Dynamic_cast(Figure* fg)
                                {  return fg -> Isa(Type_na())?
                                                    (Rectangle*)fg : 0;
                                }
            };
  class Square:public Rectangle
                  {      int data;
            public:
                    Square() { data=88; }
                      virtual char* Type_na()
                                          {  return "Square";  }
                      virtual int Isa(char* cna)
                              {  return !strcmp(cna, "Rectangle")?
                                                    1 : Rectangle::Isa(cna);
                                }
              static Square* Dynamic_cast(Figure *fg)
                              {  return fg->Isa(Type_na())?
                                                    (Square*)fg : 0;
                                }
                void Display()  {  cout << "888" << endl;  }
        };
虚函数Type_na() 提供类名称之RTTI�o而Isa() 则提供继承之RTTI�o用来支持「动态转类型」函数──Dynamic_cast()。例如�s
            Figure *f  =  new Rectangle();
            cout << f -> Isa("Square") << endl;
            cout << f -> Isa("Figure") << endl;
这些指令可显示出�sf 所指向之对象并非Square之对象�o但是Figure之对象(含子孙对象)。再如�s
            Figure *f;  Square *s;
            f  =  new Rectangle();
            s  =  Square  ==  Dynamic_cast(f);
            if(!s)
                    cout << "dynamic_cast error!!" << endl;
此时�o依RTTI来判断出这转类型是不对的。
类库提供RTTI
  由类库提供RTTI是最常见的�o例如Visual C++的MFC 类库内有个CRuntimeClass 类�o其内含简单的RTTI。请看个程序�s
              class Figure:public CObject
                    {
                            DECLARE_DYNAMIC(Figure);
                    };
      class Rectangle : public Figure
                    {
                          DECLARE_DYNAMIC(Rectangle);
                    };
      class Square : public Rectangle
              {
                    DECLARE_DYNAMIC(Square);
                    int data;
                public:
                    void Display()  {  cout << data << endl;  }
                    Square()      {  data=88;  }
                };
      IMPLEMENT_DYNAMIC(Figure, CObject);
      IMPLEMENT_DYNAMIC(Rectangle, Figure);
      IMPLEMENT_DYNAMIC(Square, Rectangle);
Visual C++程序依赖这些宏(Macor) 来支持RTTI。现在就看看如何使用CRuntimeClass
类吧�u如下�s
                  CRuntimeClass *r;
                  Figure *f  =  new Rectangle();
                  r = f -> GetRuntimeClass();
                  cout << r -> m_psClassName << endl;
        这就在执行时期得到类的名称。Visual C++的类库仅提供些较简单的RTTI──类名称、对象大小及父类等。至于其它常用的RTTI如──数据项的类型及位置(position)等皆未提供。
C++编译器提供RTTI
  由C++ 语言直接提供RTTI是最方便了�o但是因RTTI的范围随应用场合而不同�o若C++语言提供所有的RTTI�o将会大幅度增加C++ 的复杂度。目前�oC++ 语言只提供简单的RTTI�o例如Borland C++ 新增typeid()操作数以及dynamic_cast<T*>函数样版。请看个程序�s
    class Figure
              {    public:
                              virtual void Display();
                };
    class Rectangle : public Figure    { };
    class Square:public Rectangle
          {      int data;
                  public:
                      Square() {  data=88;  }
                        void Display() {  cout << data << endl;  }
            };
现在看看如何使用typeid()操作数──
                  Figure *f  =  new Square();
                  const typeinfo  ty  =  typeid(*f);
                  cout << ty.name() << endl;
这会告诉您�sf 指针所指的对象�o其类名称是Square。再看看如何使用dynamic_cast<T*>函数样版──
          Figure *f;  Square *s;
          f = new Rectangle();
          s = dynamic_cast<Sqiare *>(f);
          if(!s)
                  cout << "dynamic casting error!!" << endl;
在执行时�o发现f 是不能转为Square *类型的。如下指令�s
            Figure *f;  Rectangle *r;
            f = new Square();
            r = dynamic_cast<Rectangle *>(f);
            if(r)      r->Display();
这种类型转换是对的。
RTTI与虚函数表
在C++ 程序中�o若类含有虚函数�o则该类会有个虚函数表(Virtual Function Table�o
简称VFT )。为了提供RTTI�oC++ 就将在VFT 中附加个指针�o指向typeinfo对象�o这对象内含RTTI资料,如下图:
    由于该类所实例化之各对象�o皆含有个指针指向VFT 表�o因之各对象皆可取出typeinfo对象而得到RTTI。例如�o
                  Figure *f1 = new Square();
                  Figure *f2 = new Square();
                  const typeinfo ty = typeid(*f2);
其中�otypeid(*f2) 的动作是�s
1.取得f2所指之对象。
2.从对象取出指向VMF 之指针�o经由此指针取得VFT 表。
3.从表中找出指向typeinfo对象之指针�o经由此指针取得typeinfo对象。
  这typeinfo对象就含有RTTI了。参考下图1,经由f1及f2两指针皆可取得typeinfo对象�o所以    typeid(*f2) == typeid(*f1)。
总结
  RTTI是C++ 的新功能。过去�oC++ 语言来提供RTTI时�o大多依赖类库来支持�o但各类库使用的方法有所不同�o使得程序的可移植性(portability) 大受影响。然而�o目前C++ 也只提供最简单的RTTI而已�o可预见的未来�o当大家对RTTI的意见渐趋一致时�oC++ 将会提供更完整的RTTI�o包括数据项和成员函数的类型、位置(offset)等资料�o使得C++ 程序更井然有序�o易于维护。
参考资料
[注1]  Stroustrup B., “Run-Time Type Identification for C++”, Usenix C++ C
onference, Portland, 1993.
[注2] Meyer B.,Object-Oriented Software Construction, Prentice Hall, 1988.�

你可能感兴趣的:(C++,使用,休闲,RTTI,观念)