自从1993年Bjarne Stroustrup提出有关C++的RTTI功能之建议﹐以及C++的异常处理(exception handling)需要RTTI;最近新推出的C++或多或少已提供RTTI。然而,若不小心使用RTTI,可能会导致软件弹性的降低。本文将介绍RTTI的观念和近况﹐并说明如何善用它。
Java中任何一个类都可以通过反射机制来获取类的基本信息(接口、父类、方法、属性、Annotation等),而且Java中还提供了一个关键字instanceof,可以在运行时判断一个类是不是另一个类的子类或者是该类的对象。
和Java相比,C++要想获得运行时类型信息,只能通过RTTI机制。
在C++环境中﹐头文件(header file)含有类之定义(class definition),亦即包含有关类的结构资料(representational information)。但是﹐这些资料只供编译器(compiler)使用﹐编译完毕后并未留下来﹐所以在执行时期(at run-time)﹐无法得知对象的类资料﹐包括类名称、数据成员名称与类型、函数名称与类型等等。例如﹐有基类Figure和它的派生类Circle。
若有如下指令﹕
Figure *p; p =new Circle(); Figure &q = *p;在执行时﹐p指向一个对象﹐但欲得知此对象之类资料﹐就有困难了。同样欲得q所引用(reference)对象的类资料﹐也无法得到。RTTI(Run-Time Type Identification)就是要解决这类困难﹐也就是在执行时﹐您想知道指针所指到或引用到的对象类型时﹐该对象有能力来告诉您。随着应用场合之不同﹐所需支持的RTTI范围也不同。最单纯的RTTI包括﹕
① 类识别(class identification)──包括类名称或ID。
② 继承关系(inheritance relationship)──支持执行时期的「往下转换类型」(downward casting)﹐亦即动态类型转换(dynamic casting)。
在对象数据库存取上﹐还需要下述RTTI﹕
③ 对象结构(object layout)──包括属性的类型、名称及其位置(position或offset)。
④ 成员函数表(table of functions)──包括函数的类型、名称、及其参数类型等。
其目的是协助对象的I/O和持久化(persistence)﹐也提供调试讯息等。
若依照Bjarne Stroustrup之建议﹐C++还应包括更完整的RTTI﹕
⑤ 能得知类所实例化的各对象。
⑥ 能引用到函数的源代码。
⑦ 能取得类的有关在线说明(on-line documentation)。
其实这些都是C++编译完成时所丢弃的资料﹐如今只是希望寻找个途径来将之保留到执行期间。然而﹐要提供完整的RTTI﹐将会大幅提高C++的复杂度!
RTTI最主要的副作用是﹕程序员可能会利用RTTI来支持其「复选」(multiple-selection)方法﹐而不使用虚函数(virtual function)方法。
虽然这两种方法皆能达到多态化(polymorphism)﹐但使用复选方法﹐常导致违反著名的「开放╱封闭原则」(open/closed principle)。反之﹐使用虚函数方法则可合乎这个原则。
Circle和Square皆是由Figure所派生出来的子类﹐它们各有自己的draw()函数。当C++提供了RTTI﹐就可写个函数如下﹕
void drawing( Figure *p ) { if( typeid(*p).name() == "Circle") ((Circle*)p) -> draw(); if( typeid(*p).name() == "Rectangle" ) ((Rectangle*)p) -> draw(); }
虽然drawing()函数也具有多态﹐但它与Figure类体系的结构具有紧密的相关性。当Figure类体系再派生出子类时﹐drawing()函数的内容必须加多一个if指令。因而违反了「开放╱封闭原则」﹐如下﹕
很显然地﹐drawing()函数应加以修正。
想一想﹐如果C++并未提供RTTI﹐则程序员毫无选择必须使用虚函数来支持drawing()函数的多态。于是程序员将draw()声明为虚函数﹐并写drawing()如下﹕
void drawing(Figure *p) { p->draw(); }
如此﹐Figure类体系能随时派生类﹐而不必修正drawing()函数。亦即﹐Figure体系有个稳定的接口(interface)﹐drawing()使用这接口﹐使得drawing()函数也稳定﹐不会随Figure类体系的扩充而变动。这是封闭的一面。而这稳定的接口并未限制Figure体系的成长﹐这是开放的一面。 因而合乎「开放╱封闭」原则﹐软件的结构会更具弹性﹐更易于随环境而不断成长。
一般而言﹐RTTI的常见使用场合有四﹕异常处理(exceptions handling)、动态类型转换(dynamic casting)、模块集成、以及对象I/O。
3.1 异常处理──大家所熟悉的C++新功能﹕异常处理﹐其需要RTTI﹐如类名称等。
3.2 动态类型转换──在类体系(class hierarchy)中﹐往下的类型转换需要类继承的RTTI。
3.3 模块集成──当某个程序模块里的对象欲跟另一程序模块的对象沟通时﹐应如何得知对方的身份呢﹖知道其身份资料﹐才能调用其函数。一般的C++程序﹐常见的解决方法是──在源代码中把对方对象之类定义(即存在头文件里)包含进来﹐在编译时进行链接工作。然而﹐像目前流行的主从(Client-Server)架构中﹐客户端(client)的模块对象﹐常需与主机端(server)的现成模块对象沟通﹐它们必须在执行时沟通﹐但又常无法一再重新编译。于是靠头文件来提供的类定义资料﹐无助于执行时的沟通工作﹐只得依赖RTTI了。
3.4 对象I/O──C++程序常将其对象存入数据库﹐未来可再读取之。对象常内含其它小对象﹐因之在存入数据库时﹐除了必须知道对象所属的类名称﹐也必须知道各内含小对象之所属类﹐才能完整地将对象存进去。储存时﹐也将这些RTTI资料连同对象内容一起存入数据库中。未来读取对象时﹐可依据这些RTTI资料来分配内存空间给对象。
上述谈到RTTI的用途﹐以及其副作用。有众多争论﹐使得RTTI的标准迟迟未呈现出来。也导致各C++开发环境提供者﹐依其环境所需而以各种方式来支持RTTI﹐且其支持RTTI的范围也所不同。目前常见的支持方式包括﹕
I 由类库提供RTTI──例如﹐Microsoft公司的Visual C++环境。
II 由C++编译器(compiler)提供──例如﹐Borland C++4.5 版本。
III 由源代码产生器(code generator)提供──例如Bellvobr系统。
IV 由OO数据库的特殊预处理器(preprocessor)提供──例如Poet系统。
V 由程序员自己加上去。
这些方法皆只提供简单的RTTI﹐其仅为Stroustrup先生所建议RTTI内涵的部分集合而已。相信不久的将来﹐会由C++编译器来提供ANSI标准的RTTI﹐但何时会订出这标准呢﹖没人晓得吧!
通常程序员自己可提供简单的RTTI﹐例如提供类的名称或识别(TypeID)。最常见的方法是﹕为类体系定义些虚函数如Type_na()及Isa()函数等。请先看个例子﹕
class Figure { }; class Rectangle :public Figure { }; classSquare :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之对象﹐而s->Display()调用Square::Display()﹐将找不到data值。若在执行时能利用RTTI来检查之﹐就可发出错误讯息。于是﹐自行加入RTTI功能﹕
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﹐而Isa()则提供继承之RTTI﹐用来支持「动态类型转换」函数──Dynamic_cast()。例如﹕
Figure *f =new Rectangle(); cout << f -> Isa("Square") << endl; cout << f -> Isa("Figure") << endl;
这些指令可显示出﹕f所指向之对象并非Square之对象﹐但是Figure之对象(含子孙对象)。再如﹕
Figure *f; Square *s; f =new Rectangle(); s = Square == Dynamic_cast(f); if(!s) cout << "dynamic_cast error!!" << endl;
此时﹐依RTTI来判断出这转类型是不对的。
由类库提供RTTI是最常见的﹐例如Visual C++的MFC类库内有个CRuntimeClass类﹐其内含简单的RTTI。请看个程序﹕
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类吧!如下﹕
CRuntimeClass *r; Figure *f =new Rectangle(); r = f -> GetRuntimeClass(); cout << r -> m_psClassName << endl;
这就在执行时期得到类的名称。Visual C++的类库仅提供些较简单的RTTI──类名称、对象大小及父类等。至于其它常用的RTTI如──数据项的类型及位置(position)等皆未提供。
由C++语言直接提供RTTI是最方便了﹐但是因RTTI的范围随应用场合而不同﹐若C++语言提供所有的RTTI﹐将会大幅度增加C++的复杂度。目前﹐C++语言只提供简单的RTTI﹐例如Borland C++新增typeid()操作数以及dynamic_cast<T*>函数样版。请看个程序﹕
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 type_info ty = typeid(*f); cout << ty.name() << endl;
这会告诉您﹕f 指针所指的对象﹐其类名称是Square。ic_cast
Figure *f; Square *s; f =new Rectangle(); s = dynamic_cast<Sqiare *>(f); if(!s) cout << "dynamic casting error!!" << endl;
在执行时﹐发现f是不能转为Square *类型的。如下指令﹕
Figure *f; Rectangle *r; f =new Square(); r = dynamic_cast<Rectangle *>(f); if(r) r->Display();
这种类型转换是对的。
在C++程序中﹐若类含有虚函数﹐则该类会有个虚函数表(Virtual Function Table﹐简称VFT)。为了提供RTTI﹐C++就将在VFT中附加个指针﹐指向type_info对象﹐这对象内含RTTI资料。
由于该类所实例化之各对象﹐皆含有个指针指向VFT表﹐因之各对象皆可取出type_info对象而得到RTTI。例如﹐
Figure *f1 =new Square(); Figure *f2 =new Square(); const type_info ty = typeid(*f2);
其中﹐typeid(*f2)的动作是﹕
1.取得f2所指之对象。
2.从对象取出指向VMF之指针﹐经由此指针取得VFT表。
3.从表中找出指向type_info对象之指针﹐经由此指针取得type_info对象。
这type_info对象就含有RTTI了。经由f1及f2两指针皆可取得type_info对象﹐所以typeid(*f2) == typeid(*f1)。
我们知道C++的多态性(运行时)是由虚函数实现的,对于多态性的对象,无法在程序编译阶段确定对象的类型(运行阶段动态确定,如由用户自行指定)。当类中含有虚函数时,其基类的指针就可以指向任何派生类的对象,这时就有可能不知道基类指针到底指向的是哪个对象的情况,类型的确定要在运行时利用运行时类型标识做出。为了获得一个对象的类型可以使用typeid函数,该函数反回一个对type_info类对象的引用,要使用typeid必须使用头文件
下面是typeinfo的源代码:
/*** *typeinfo.h - Defines the type_info structure and exceptions used for RTTI * * Copyright (c) Microsoft Corporation. All rights reserved. * Modified January 1996 by P.J. Plauger * *Purpose: * Defines the type_info structure and exceptions used for * Runtime Type Identification. * * [Public] * ****/ #pragma once #ifndef _TYPEINFO_ #define _TYPEINFO_ #ifndef RC_INVOKED #include <xstddef> #include <string.h> // for type_info::hash_code() #pragma pack(push,_CRT_PACKING) #pragma warning(push,3) #pragma push_macro("new") #undef new #pragma warning(disable: 4275) #ifndef __cplusplus #error This header requires a C++ compiler ... #endif #if !defined(_WIN32) #error ERROR: Only Win32 target supported! #endif struct__type_info_node { void *_MemPtr; __type_info_node* _Next; }; extern __type_info_node __type_info_root_node; class type_info { public: size_t hash_code() const _THROW0() { // hash name() to size_t value by pseudorandomizing transform return (_STD _Hash_seq((const unsigned char *) name(), _CSTD strlen(name()))); } #if defined(CRTDLL) && defined(_CRTBLD) _CRTIMP_PURE #endif #ifdef _M_CEE [System::Security::SecurityCritical] #endif virtual ~type_info() _NOEXCEPT; #if defined(_SYSCRT) _CRTIMP_PURE int __CLR_OR_THIS_CALL operator==(const type_info& _Rhs) const; _CRTIMP_PURE int __CLR_OR_THIS_CALL operator!=(const type_info& _Rhs) const; #else _CRTIMP_PURE bool __CLR_OR_THIS_CALL operator==(const type_info& _Rhs) const; _CRTIMP_PURE bool __CLR_OR_THIS_CALL operator!=(const type_info& _Rhs) const; #endif _CRTIMP_PURE bool __CLR_OR_THIS_CALL before(const type_info& _Rhs) const; _CRTIMP_PURE const char* __CLR_OR_THIS_CALL name(__type_info_node* __ptype_info_node = &__type_info_root_node) const; _CRTIMP_PURE const char* __CLR_OR_THIS_CALL raw_name() const; private: void *_M_data; char _M_d_name[1]; #if defined(_CRTBLD) /* TRANSITION */ __CLR_OR_THIS_CALL type_info(const type_info& _Rhs); type_info& __CLR_OR_THIS_CALL operator=(const type_info& _Rhs); #else public: __CLR_OR_THIS_CALL type_info(const type_info&) = delete; type_info& __CLR_OR_THIS_CALL operator=(const type_info&) = delete; private: #endif _CRTIMP_PURE static const char *__CLRCALL_OR_CDECL _Name_base(const type_info *,__type_info_node* __ptype_info_node); _CRTIMP_PURE static void __CLRCALL_OR_CDECL _Type_info_dtor(type_info *); #if defined(_CRTBLD) #if !defined(_SYSCRT) _CRTIMP_PURE static const char *__CLRCALL_OR_CDECL _Name_base_internal(const type_info *,__type_info_node* __ptype_info_node); _CRTIMP_PURE static void __CLRCALL_OR_CDECL _Type_info_dtor_internal(type_info *); public: // CRT dll import libs alias non _internal to _internal. These method definitions are // only used within the crtdll to provide targets for aliasobj in the crt import lib. _CRTIMP_PURE void __CLR_OR_THIS_CALL _type_info_dtor_internal_method(void); _CRTIMP_PURE const char* __CLR_OR_THIS_CALL _name_internal_method(__type_info_node* __ptype_info_node) const; #endif #endif }; #if _HAS_EXCEPTIONS _STD_BEGIN using ::type_info; _STD_END #if !defined(_CRTBLD) || !defined(_TICORE) // This include must occur below the definition of class type_info #include <exception> _STD_BEGIN class _CRTIMP_PURE bad_cast : public exception { public: #ifdef _M_CEE_PURE __CLR_OR_THIS_CALL bad_cast(const char * _Message = "bad cast") : exception(_Message) {} __CLR_OR_THIS_CALL bad_cast(const bad_cast &_That) : exception(_That) {} virtual __CLR_OR_THIS_CALL ~bad_cast() _NOEXCEPT {} #if defined(_CRTBLD) && defined(CRTDLL) private: // This is aliased to public:bad_cast(const char * const &) to provide // the old, non-conformant constructor. __CLR_OR_THIS_CALL bad_cast(const char * const * _Message) : exception((const char *)_Message) { } #endif /* _CRTBLD && CRTDLL */ #else /* _M_CEE_PURE */ __CLR_OR_THIS_CALL bad_cast(const char * _Message = "bad cast"); __CLR_OR_THIS_CALL bad_cast(const bad_cast &); virtual __CLR_OR_THIS_CALL ~bad_cast() _NOEXCEPT; #if defined(_CRTBLD) && defined(CRTDLL) private: // This is aliased to public:bad_cast(const char * const &) to provide // the old, non-conformant constructor. __CLR_OR_THIS_CALL bad_cast(const char * const * _Message); #endif /* _CRTBLD && CRTDLL */ #endif /* _M_CEE_PURE */ }; class _CRTIMP_PURE bad_typeid : public exception { public: #ifdef _M_CEE_PURE __CLR_OR_THIS_CALL bad_typeid(const char * _Message = "bad typeid") : exception(_Message) {} __CLR_OR_THIS_CALL bad_typeid(const bad_typeid &_That) : exception(_That) {} virtual __CLR_OR_THIS_CALL ~bad_typeid() _NOEXCEPT {} #else /* _M_CEE_PURE */ __CLR_OR_THIS_CALL bad_typeid(const char * _Message = "bad typeid"); __CLR_OR_THIS_CALL bad_typeid(const bad_typeid &); virtual __CLR_OR_THIS_CALL ~bad_typeid() _NOEXCEPT; #endif /* _M_CEE_PURE */ }; class _CRTIMP_PURE __non_rtti_object : public bad_(typeid { public: #ifdef _M_CEE_PURE __CLR_OR_THIS_CALL __non_rtti_object(const char * _Message) : bad_typeid(_Message) {} __CLR_OR_THIS_CALL __non_rtti_object(const __non_rtti_object &_That) : bad_typeid(_That) {} virtual __CLR_OR_THIS_CALL ~__non_rtti_object() _NOEXCEPT {} #else /* _M_CEE_PURE */ __CLR_OR_THIS_CALL __non_rtti_object(const char * _Message); __CLR_OR_THIS_CALL __non_rtti_object(const __non_rtti_object &); virtual __CLR_OR_THIS_CALL ~__non_rtti_object() _NOEXCEPT; #endif /* _M_CEE_PURE */ }; _STD_END #endif // !_CRTBLD || !_TICORE #else _STD_BEGIN // CLASS bad_cast class _CRTIMP2 bad_cast : public exception { // base of all bad cast exceptions public: bad_cast(const char *_Message = "bad cast") _THROW0() : exception(_Message) { // construct from message string } virtual ~bad_cast() _NOEXCEPT { // destroy the object } protected: virtual void _Doraise() const { // perform class-specific exception handling _RAISE(*this); } }; // CLASS bad_typeid class _CRTIMP2 bad_typeid : public exception { // base of all bad typeid exceptions public: bad_typeid(const char *_Message = "bad typeid") _THROW0() : exception(_Message) { // construct from message string } virtual ~bad_typeid() _NOEXCEPT { // destroy the object } protected: virtual void _Doraise() const { // perform class-specific exception handling _RAISE(*this); } }; class _CRTIMP2 __non_rtti_object : public bad_typeid { // report a non RTTI object public: __non_rtti_object(const char *_Message) : bad_typeid(_Message) { // construct from message string } }; _STD_END #endif /* _HAS_EXCEPTIONS */ #endif /* RC_INVOKED */ #pragma pop_macro("new") #pragma pack(pop) #pragma warning(pop) #endif // _TYPEINFO_ /* * Copyright (c) Microsoft Corporation. ALL RIGHTS RESERVED. * Modified January 1996 by P.J. Plauger * Modified November 1998 by P.J. Plauger * Consult your license regarding permissions and restrictions. V6.00:0009 */
class type_info { public: //析构函数 _CRTIMP virtual ~type_info(); //重载的==操作符 _CRTIMP int operator==(const type_info& rhs) const; //重载的!=操作符 _CRTIMP int operator!=(const type_info& rhs) const; _CRTIMP int before(const type_info& rhs) const;//用于type_info对象之间的排序算法 //返回类的名字 _CRTIMP const char* name() const; _CRTIMP const char* raw_name() const;//返回类名称的编码字符串 private: //各种存储数据成员 void *_m_data; char _m_d_name[1]; //将拷贝构造函数与赋值构造函数设为了私有 type_info(const type_info& rhs); type_info& operator=(const type_info& rhs); };
因为type_info类的复制构造函数和赋值运算符都是私有的,所以不允许用户自已创建type_info的类。可以使用typeid函数返回一个type_info引用。
多态类的对象的类型信息保存在一个type_info对象中,该对象的地址保存在虚函数表的索引的-1的项中,该type_info对象保存着该对象对应的类型信息,每个类都对应着一个type_info对象。
#include <iostream> using namespace std; class X{ public: X(){ mX = 101;} virtual void vfunc(){ cout << "X::vfunc()" << endl;} private: int mX; }; class XX : public X{ public: XX():X(){ mXX = 1001;} virtual void vfunc(){ cout << "XX::vfunc()" << endl;} private: int mXX; }; typedef void (*FuncPtr)(); int main() { XX xx; FuncPtr func; char *p = (char*)&xx; // 获得虚函数表的地址 int **vtbl = (int**)*(int**)p; // 输出虚函数表的地址,即vptr的值 cout << vtbl << endl; // 获得type_info对象的指针,并调用其name成员函数 cout << "\t[-1]: " ; cout << (vtbl[-1]) << " -> "; cout << ((type_info*)(vtbl[-1]))->name() << endl; // 调用第一个virtual函数 cout << "\t[0]: " << vtbl[0] << " -> "; func = (FuncPtr)vtbl[0]; func(); // 输出基类的成员变量的值 p += sizeof(int**); cout << *(int*)p << endl; // 输出派生类的成员变量的值 p += sizeof(int); cout << *(int*)p << endl; return 0; }
/*运行结果 0x402048 [-1]: 0x402068 -> 2XX [0]: 0x40134e -> XX::vfunc() 101 1001 */
当typeid中的操作数是如下情况之一时,typeid运算符需要在程序运行时计算类型,因为其其操作数的类型在编译时期是不能被确定的。
(1)一个指向不含有virtual函数的类对象的指针的解引用
(2)一个指向不含有virtual函数的类对象的引用
多态的类型是可以在运行过程中被改变的,例如,一个基类的指针,在程序运行的过程中,它可以指向一个基类对象,也可以指向该基类的派生类的对象,而typeid运算符需要在运行过程中识别出该基类指针所指向的对象的实际类型,这就需要typeid运算符在运行过程中计算其指向的对象的实际类型
#include <iostream> using namespace std; class X{ public: X(){ mX = 101;} virtual void vfunc(){ cout << "X::vfunc()" << endl;} private: int mX; }; class XX : public X{ public: XX():X(){ mXX = 1001;} virtual void vfunc(){ cout << "XX::vfunc()" << endl;} private: int mXX; }; void printTypeInfo(const X *px) { cout << "typeid(px) -> " << typeid(px).name() << endl; cout << "typeid(*px) -> " << typeid(*px).name() << endl; } int main() { X x; XX xx; printTypeInfo(&x); printTypeInfo(&xx); return 0; }
/* typeid(px) -> PK1X typeid(*px) -> 1X typeid(px) -> PK1X typeid(*px) -> 2XX */
RTTI是C++的新功能。过去﹐C++语言来提供RTTI时﹐大多依赖类库来支持﹐但各类库使用的方法有所不同﹐使得程序的可移植性(portability)大受影响。然而﹐目前C++也只提供最简单的RTTI而已﹐可预见的未来﹐当大家对RTTI的意见渐趋一致时﹐C++将会提供更完整的RTTI﹐包括数据项和成员函数的类型、位置(offset)等资料﹐使得C++程序更井然有序﹐易于维护。
[注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
http://www.cnblogs.com/yc_sunniwell/archive/2010/07/07/1773068.html