C++的RTTI和dynamic_cast效率问题

在网上经常看到有人说,dynamic_cast的效率问题.当然因为它是运行 时的cast,开销必然少不了.
对于down cast,可以用static_cast代替,只不过不太安全.
对于钻石结构类关系,要cast到兄弟类,那么只能用dynamic_cast了.

记得我在做前一个项目中,发现召唤了500个一样的怪物的时候客户端巨卡,于是把可以优化的地方都搞了一遍,包括把dynamic_cast改为 static_cast.最后问题解决了,这也让我对dynamic_cast有了实践性的感知,而不是停留在道听途说的那种认识.

今天跟同事讨论dynamic_cast,同时我也觉得Gamebryo的NiIsKindOf和NiDynamicCast的效率跟C++的效率 差不多,都是运行时的类型判断和转换,但是同事跟我说,C++的dynamic_cast会比较虚函数表,也就是一层一层匹配v-table,开销巨大, 而Gamebryo的RTTI没有这么大的开销.我带着疑问做了测试发现如下结果:

在相同的软件和硬件环境下,相同的循环次数下,并且都是Release版本:
1.typeid() 类型判断与NiIsKindOf的比较
typeid用了9秒多,NiIsKindOf用了2秒多.


2.dynamic_cast与NiDyanmicCast:
dymaic_cast用了26.7秒,NiDyamicCast用了3.3秒, 8倍!

看到以上结果,我顿时大吃一惊,明白了为什么很多引擎用自己实现的RTTI而不用C++的.比如Nebula引擎也有类似的RTTI,当时我看到了还很不 理解.
实践是检验真理的唯一标准,呵呵,以前不管别人怎么说,我是带有不确定的疑问,"真的是这样吗",今天测了才知道确实不一样.
突然想起来,这个测试可能不准,因为编译模式里面把C++内置的RTTI关掉了,会不会影响效率呢.有待验证..

另我对于Gamebryo或者其他类似的RTTI的看法,
第一,不雅观,为了方便把宏丢到类声明里面,宏本身我就觉得不雅,如果放在逻辑执行块里面我还勉强能接受,放在类声明里面,至少我肯定不会这么做的,呵 呵.

第二,更重要的,Effective C++上说,尽量避免dyamic_cast,不光是运行时的开销,而且对于设计,也不太好.用到dynamic_cast的设计,不能算是好的设计.应 该在对象抽象上做好统一的接口.因为IsKindOf或者dynamic_cast都是依赖于具体类型的,面向过程的,相当于一个if else,而不是面向对象的.

如何在运行时确定对象类型(RTTI)

    RTTI 是“Runtime Type Information”的缩写,意思是:运行时类型信息。它提供了运行时确定对象类型的方法。本文将简略介绍 RTTI 的一些背景知识、描述 RTTI 的概念,并通过具体例子和代码介绍什么时候使用以及如何使用 RTTI;本文还将详细描述两个重要的 RTTI 运算符的使用方法,它们是 typeid 和 dynamic_cast。
    其实,RTTI 在C++中并不是什么新的东西,它早在十多年以前就已经出现了。但是大多数开发人员,包括许多高层次的C++程序员对它并不怎么熟悉,更不用说使用 RTTI 来设计和编写应用程序了。
    一些面向对象专家在传播自己的设计理念时,大多都主张在设计和开发中明智地使用虚拟成员函数,而不用 RTTI 机制。但是,在很多情况下,虚拟函数无法克服本身的局限。每每涉及到处理异类容器和根基类层次(如 MFC)时,不可避免要对对象类型进行动态判断,也就是动态类型的侦测。如何确定对象的动态类型呢?答案是使用内建的 RTTI 中的运算符:typeid 和 dynamic_cast。
    首先让我们来设计一个类层次,假设我们创建了某个处理文件的抽象基类。它声明下列纯虚拟函数:open()、close()、read()和 write():

class File
{
public:
 virtual int open(const string & filename)=0;
 virtual int close(const string & filename)=0;
 //
 virtual ~File()=0; // 记住添加纯虚拟析构函数(dtor)
};
现在从 File 类派生的类要实现基类的纯虚拟函数,同时还要提供一些其他的操作。假设派生类为 DiskFile,除了实现基类的纯虚拟函数外,还要实现自己的flush()和defragment()操作:
class DiskFile: public File
{
public:
 int open(const string & filename);

 // 实现其他的纯虚拟函数
    ......

 // 自己的专有操作
 virtual int flush();
 virtual int defragment();
};    
接着,又从 DiskFile 类派生两个类,假设为 TextFile 和 MediaFile。前者针对文本文件,后者针对音频和视频文件:
class TextFile: public DiskFile
{
  // ......
  int  sort_by_words();
};

class MediaFile: public DiskFile
{
  //......
};        
我们之所以要创建这样的类层次,是因为这样做以后可以创建多态对象,如:
File *pfile; // *pfile的静态类型是 File
if(some_condition)
  pfile = new TextFile; // 动态类型是 TextFile
else
  pfile = new DiskFile; // 动态类型是 DiskFile       
假设你正在开发一个基于图形用户界面(GUI)的文件管理器,每个文件都可以以图标方式显示。当鼠标移到图标上并单击右键时,文件管理器打开一个菜单,每 个文件除了共同的菜单项,不同的文件类型还有不同的菜单项。如:共同的菜单项有“打开”“拷贝”、和“粘贴”,此外,还有一些针对特殊文件的专门操作。比 如,文本文件会有“编辑”操作,而多媒体文件则会有“播放”菜单。为了使用 RTTI 来动态定制菜单,文件管理器必须侦测每个文件的动态类型。利用 运算符 typeid 可以获取与某个对象关联的运行时类型信息。typeid 有一个参数,传递对象或类型名。因此,为了确定 x 的动态类型是不是Y,可以用表达式:typeid(x) == typeid(Y)实现:
#include <typeinfo> // typeid 需要的头文件
void menu::build(const File * pfile)
{
 if (typeid(*pfile)==typeid(TextFile))
 {
  add_option("edit"); 
 }
 else if (typeid(*pfile)==typeid(MediaFile))
 {
 add_option("play"); 
 }
}      
使用 typeid 要注意一个问题,那就是某些编译器(如 Visual C++)默认状态是禁用 RTTI 的,目的是消除性能上的开销。如果你的程序确实使用了 RTTI,一定要记住在编译前启用 RTTI。使用 typeid 可能产生一些将来的维护问题。假设你决定扩展上述的类层次,从MediaFile 派生另一个叫 LocalizeMedia 的类,用这个类表示带有不同语言说明文字的媒体文件。但 LocalizeMedia 本质上还是个 MediaFile 类型的文件。因此,当用户在该类文件图标上单击右键时,文件管理器必须提供一个“播放”菜单。可惜 build()成员函数会调用失败,原因是你没有检查这种特定的文件类型。为了解决这个问题,你必须象下面这样对 build() 打补丁:
void menu::build(const File * pfile)
{

 //......

  else if (typeid(*pfile)==typeid(LocalizedMedia))
 {
  add_option("play"); 
  }
}     
唉,这种做法真是显得太业余了,以后每次添加新的类,毫无疑问都必须打类似的补丁。显然,这不是一个理想的解决方案。这个时候我们就要用到 dynamic_cast,这个运算符用于多态编程中保证在运行时发生正确的转换(即编译器无法验证是否发生正确的转换)。用它来确定某个对象是 MediaFile 对象还是它的派生类对象。dynamic_cast 常用于从多态编程基类指针向派生类指针的向下类型转换。它有两个参数:一个是类型名;另一个是多态对象的指针或引用。其功能是在运行时将对象强制转换为目 标类型并返回布尔型结果。也就是说,如果该函数成功地并且是动态的将 *pfile 强制转换为 MediaFile,那么 pfile的动态类型是 MediaFile 或者是它的派生类。否则,pfile 则为其它的类型:
void menu::build(const File * pfile)
{
 if (dynamic_cast <MediaFile *> (pfile))
 {
  // pfile 是 MediaFile 或者是MediaFile的派生类 LocalizedMedia
  add_option("play"); 
 }
 else if (dynamic_cast <TextFile*> (pfile))
 {
  // pfile 是 TextFile 是TextFile的派生类
  add_option("edit"); 
 }
}    
细细想一下,虽然使用 dynamic_cast 确实很好地解决了我们的问题,但也需要我们付出代价,那就是与 typeid 相比,dynamic_cast 不是一个常量时间的操作。为了确定是否能完成强制类型转换,dynamic_cast`必须在运行时进行一些转换细节操作。因此在使用 dynamic_cast 操作时,应该权衡对性能的影响。
  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第0章 导读(译者的话) 第1章 关于对象(Object Lessons) 加上封装后的布局成本(Layout Costs for Adding Encapsulation) 1.1 C++模式模式(The C++ Object Model) 简单对象模型(A Simple Object Model) 表格驱动对象模型(A Table-driven Object Model) C++对象模型(Th e C++ Object Model) 对象模型如何影响程序(How the Object Model Effects Programs) 1.2 关键词所带来的差异(A Keyword Distinction) 关键词的困扰 策略性正确的struct(The Politically Correct Struct) 1.3 对象的差异(An Object Distinction) 指针的类型(The Type of a Pointer) 加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics of constructors) 2.1 Default Constructor的建构操作 “带有Default Constructor”的Member Class Object “带有Default Constructor”的Base Class “带有一个Virual Function”的Class “带有一个virual Base class”的Class 总结 2.2 Copy Constructor的建构操作 Default Memberwise Initialization Bitwise Copy Semantics(位逐次拷贝) 不要Bitwise Copy Semantics! 重新设定的指针Virtual Table 处理Virtual Base Class Subobject 2.3 程序转换语意学(Program Transformation Semantics) 明确的初始化操作(Explicit Initialization) 参数的初始化(Argument Initialization) 返回值的初始化(Return Value Initialization) 在使用者层面做优化(Optimization at the user Level) 在编译器层面做优化(Optimization at the Compiler Level) Copy Constructor:要还是不要? 摘要 2.4 成员们的初始化队伍(Member Initialization List) 第3章 Data语意学(The Semantics of Data) 3.1 Data Member的绑定(The Binding of a Data Member) 3.2 Data Member的布局(Data Member Layout) 3.3 Data Member的存取 Static Data Members Nonstatic Data Member 3.4 “继承”与Data Member 只要继承不要多态(Inheritance without Polymorphism) 加上多态(Adding Polymorphism) 多重继承(Multiple Inheritance) 虚拟继承(Virtual Inheritance) 3.5 对象成员的效率(Object Member Efficiency) 3.6 指向Data Members的指针(Pointer to Data Members) “指向Members的指针”的效率问题 第4章 Function语意学(The Semantics of Function) 4.1 Member的各种调用方式 Nonstatic Member Functions(非静态成员函数) Virtual Member Functions(虚拟成员函数) Static Member Functions(静态成员函数) 4.2 Virtual Member Functions(虚拟成员函数) 多重继承下的Virtual Functions 虚拟继承下的Virtual Functions 4.3 函数的效能 4.4 指向Member Functions的指针(Pointer-to-Member Functions) 支持“指向Virtual Member Functions”之指针 在多重继承之下,指向Member Functions的指针 “指向Member Functions之指针”的效率 4.5 Inline Functions 形式对数(Formal Arguments) 局部变量(Local Variables) 第5章 构造、解构、拷贝 语意学(Semantics of Construction,Destruction,and Copy) 纯虚拟函数的存在(Presence of a Pure Virtual Function) 虚拟规格的存在(Presence of a Virtual Specification) 虚拟规格中const的存在 重新考虑class的声明 5.1 无继承情况下的对象构造 抽象数据类型(Abstract Data Type) 为继承做准备 5.2 继承体系下的对象构造 虚拟继承(Virtual Inheritance) 初始化语意学(The Semantics of the vptr Initialization) 5.3 对象复制语意学(Object Copy Semantics) 5.4 对象的功能(Object Efficiency) 5.5 解构语意学(Semantics of Destruction) 第6章 执行期语意学(Runting Semantics) 6.1 对象的构造和解构(Object Construction and Destruction) 全局对象(Global Objects) 局部静态对象(Local Static Objects) 对象数组(Array of Objects) Default Constructors和数组 6.2 new和delete运算符 针对数组的new语意 Placement Operator new的语意 6.3 临时性对象(Temporary Objects) 临时性对象的迷思(神话、传说) 第7章 站在对象模型的类端(On the Cusp of the Object Model) 7.1 Template Template的“具现”行为(Template Instantiation) Template的错误报告(Error Reporting within a Template) Template中的名称决议方式(Name Resolution within a Template) Member Function的具现行为(Member Function Instantiation) 7.2 异常处理(Exception Handling) Exception Handling快速检阅 对Exception Handling的支持 7.3 执行期类型识别(Runtime Type Identification,RTTI) Type-Safe Downcast(保证安全的向下转型操作) Type-Safe Dynamic Cast(保证安全的动态转型) References并不是Pointers Typeid运算符 7.4 效率有了,弹性呢? 动态共享函数库(Dynamic Shared Libraries) 共享内存(Shared Memory)
一、下载须知: .................本书无目录 .................本书经过内容识别处理,所以好处就是书上的案例源码可以直接粘贴复制到编辑器中,当然有个别括号什么的可能需要自己纠正一下。 ................本书高清 ...............本书是和代码结合在一起讲解的,其中也讲到了运行时类的识别等等。 二、截取部分章节 《第18章:运行时类型识别》 RTTI的两种使用方法 使用RT T I有两种不同的方法。第一种就像s i z e o f ( ),因为它看上就像一个函数。但实际上它是 由编译器实现的。t y p e i d ( )带有一个参数,它可以是一个对象引用或指针,返回全局t y p e i n f o类的 常量对象的一个引用。可以用运算符“= =”和“!=”来互相比较这些对象。也可以用n a m e ( )来 获得类型的名称。注意,如果给t y p e i d ( )传递一个s h a p e *型参数,它会认为类型为s h a p e *,所以如 果想知道一个指针所指对象的精确类型,我们必须逆向引用这个指针。比如,s是个s h a p e * , cout << typeid(*s).name()<<endl; 将显示出s所指向的对象类型。 也可以用b e f o r e ( t y p e i n f o & )查询一个t y p e i n f o对象是否在另一个t y p e i n f o对象的前面(以定 义实现的排列顺序),它将返回t r u e或f a l s e。如果写: if(typeid(me).before(typeid(you))) //... 那么表示我们正在查询m e在排列顺序中是否在y o u之前。 RT T I的第二个用法叫“安全类型向下映射”。之所以用“向下映射”这个词也是由于类继 承的排列顺序。如果映射一个c i r c l e *到s h a p e *叫向上映射的话,那么将一个s h a p e *映射成一个 c i r c l e *就叫向下映射了。当然一个c i r c l e *也是一个s h a p e *,编译器允许任意的向上映射,但一 个s h a p e *不一定就是c i r c l e *,所以编译器在没有明确的类型映射时并不允许我们完成一个向下 映射任务。当然可以用原有的C风格的类型映射或C + +的静态映射(s t a t i c c a s t ,将在本章末介绍) 来强制执行,这等于在说:“我希望它实际上是一个c i r c l e *,而且我打算要求它是。”由于并没 有明确地知道它实际上是c i r c l e,因此这样做是很危险的。在开发商制定的RT T I中一般的方法 是:创建一个函数来试着将s h a p e *指派为一个c i r c l e * (在本例中),检查执行过程中的数据类型。 如果这个函数返回一个地址,则成功;如果返回n u l l,说明我们并没有一个c i r c l e *对象。 C + +的RT T I的“安全类型向下映射”就是按照这种“试探映射”函数的格式,但它(非常 合理地)用模板语法来产生这个特殊的动态映射函数( d y n a m i c c a s t),所以本例变成: 动态映射的模板参数是我们想要该函数创建的数据类型,也就是这个函数的返回值。函数 参数是我们试图映射的源数据类型。 通常只要对一种类型作这种工作(比如将三角型变成紫色),但如果想算出各种s h a p e的数 目,可以用面下例子的框架: 当然这是人为的—我们可能已经在各个类型中放了一个静态数据成员并在构造函数中对 第18章运行时类型识别383 下载 shape* sp = new circle; circle* cp = dynamic_cast<circle*>(sp); if(cp) cou七<< "cas 七successful"; circle* cp = dynamic_cas区circle*>(sh); square* sp = dynamic_cast<square*>(sh); triangle* 七p = dynamic_cast<triangle*>(sh); 它自增。如果可以控制类的源代码并可以修改它,当然可以这样做。下面这个例子用来计算 s h a p e的个数,它用了静态数据成员和动态映射两种方法: 384 C + +编程思想 下载 //: RTSHAPES.CPP -- Counting shapes #include <iostream.h> #include< 七ime.h> #include <typeinfo.h> #include"·.\14\tstash.h" class shape { protected: S 七atic in七coun店 public: shape() { count++; vir七ual -shape() = O { count--; } vir七ual void draw () const = O; static in七quan七让y() { return count; }; in七shape : : count = O; class rectangle : public shape void operator=(rectangle&); // Disallow protected: static int count; public: rectangle() { count++; rectangle(const rectangle&) { count++;} -rectangle() { count--; } void draw () cons 七{ cout << "rec 七angle: : draw ()" << endl; S 七atic int quantity() { return count; } ... ' int rectangle: : count = 0; class ellipse : public shape void operator=(ellipse&}; // Disallow protected: static int count; public: ellipse () { count++; } ellipse (const ellipse&) { count++; } ~ellipse() { count 一;} void draw() cons 七{ cout << "ellipse: :draw()" << endl; 第18章运行时类型识别385 下载 } static int quantity() { return count; } }; int ellipse::count = O; class circle: public ellipse { void operator=(circle&); // Disallow protected: static int count; public: circle() { count++; } circle(cons 七circle&) { count++; } 一circle() { count--; } void draw() const { cout << "circle: :draw()" << endl; } static in七quantity() { return count; } } ; int circle::count = 0; main() { 七stash<shape> shapes; time_t t; II Seed random number generator: srand((unsigned)time(&t)); const mod= 12; for(int i = 0; i < rand() 令mod; i++) shapes.add(new rectangle); for(int j = O; j < rand() % mod; j++) shapes.add(new ellipse); for(int k = O; k < rand() 兮mod; k++) shapes.add(new circle); int Ncircles = O; int Nellipses = O; int Nrects = O; int Nshapes = O; for(int u = O; u < shapes.count(); u++) { shapes[u]->draw(); if(dynamic_cast<circle*>(shapes[u])) Ncircles++; if(dynamic_cast<ellipse*>(shapes[u])) Nellipses++; if(dynamic_cast<rectangle*>(shapes[u])) Nrects++; 对于这个例子,两种方法都是可行的,但静态数据成员方法只能用于我们拥有源代码并已 安装了静态数据成员和成员函数时(或者开发商已为我们提供了这些),另外RT T I可能在不同 的类中用法不同。 18.3 语法细节 本节详细介绍RT T I的两种形式是如何运行的以及两者之间的不同。 18.3.1 对于内部类型的typeid() 为了保持一致性, t y p e i d ( )也可以运用于内部类型,所以下面的表达式结果为t r u e: 18.3.2 产生合适的类型名字 t y p e i d ( )必须在所有的状况下都可以运行,比方说,下面的类中包含了一个嵌套类: 386 C + +编程思想 下载 } if(dynamic_cast<shape*>(shapes[u])) Nshapes++; cout << endl << endl <<"circles=• << Ncircles << endl <<"ellipses=• << Nellipses << endl << "rec 七angles="<< Nrects << endl <<"shapes="<< Nshapes << endl << endl << "circle: :quantity() =• << circle: :quantity() << endl << "ellipse: :quantity() = " << ellipse: :quantity() << endl << "rectangle: :quantity() =" << rectangle: : quantity () << endl << "shape: :quantity() =• << shape: :quantity() << endl;

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值