C++中的dynamic_cast
dynamic_cast有什么用?实际上,dynamic_cast是ANSI C++中仅有的两个与RTTI (Run Time Type Identification) 有关的用法之一。C++的类继承,使得有时很难弄清楚你正在使用的object属于哪个class,特别是当继承树比较深并且比较复杂的时候,例如,当你在程序中取得一个CWnd*指针,你的意图是,如果它实际上指向一个dialog对象那么就调用它的DoModal方法,这个时候,你就需要dynamic_cast:
CWnd* pWin = myGetWin();
CDialog* pDlg = NULL:
if( pDlg = dynamic_cast<CDialog*>(pWin) )
pDlg->DoModal();
这里用简单的强制类型转换或者static_cast不行吗?确实是不行的。假如pWin实际指向的是一个View对象,你的程序就会对View对象调用DoModal(),在MFC里你这样做或许仅仅会得到一个Assert,有些场合会得到一个segment fault,而按照Effective C++里面的说法,这样undefined的用法,也许会导致这个程序向你热恋中的女友发一封绝交信,呵呵。如果是多线程的程序,这样的一个问题导致你花上一整天时间疯狂地打log是很正常的事情。
而使用dynamic_cast,如果实际对象不是CDialog,一个NULL指针会被传回来。就算你忘记了写if then,你也可以很快利用调试器定位出错误在哪里。NULL指针的bug大概是所有bug中最幸福的一种。
dynamic_cast做到这一点是利用了编译器提供的RTTI机制,编译器会把一个class的类型信息放在C++ Runtime系统的某处,常常就是在vtbl的末端,这样从class指针得到vptr,再从vptr得到vtbl,就能够检查类型信息是否匹配。
dynamic_cast的用法之所以少见,是因为它实际上是一种“不好的”用法,某种程度上破坏了O-O的一些基本原则。既然定义了一个基类类型,就是想把派生类的差异性隐藏起来,提供一个统一的interface,那么你又有什么理由再从外面把这种差异性还原出来呢?不止一本C++经典论述中提到过,在一个设计良好的类继承体系中,不该为dynamic_cast留下生存空间。
然而,圣经和实际生活总有距离,有时候架构不那么完美,只是把一堆对象用一个基类指针的数组管理起来,你必须使用某个派生类的特定方法,而类库又没有为你提供这样的路径,你就只好自己动手,把它解构。像上面给出的例子,很难说在实践中不会遇见这样的状况。
也许有人会说,这样的问题啊,我会自己处理的,用不着什么RTTI,我会为在基类中添加一个type数据成员,再做一个GetType()方法,每个派生类对象在构造的时候赋予不同的值,调用的时候判断一下,这不就OK了吗?这样做当然也可以,但是首先,你的实现效率比编译器的RTTI实现差远了,你要为每个object增加一个字的空间开销,而编译器则是对一个class增加一点空间,因为编译器可以利用现有的vptr和vtbl,你却没法控制C++ Runtime库。其次,退一万步讲,为什么要设计C++呢?完全可以用纯正的C程序实现类封装、继承、多态这些机制,无非就是用一堆函数指针嘛!早期的C++程序是怎么编译的?是用一个预处理器先翻译成C代码,再用C编译器去编译。用了C++,不就是为了少写一些代码,程序结构更清楚嘛。因此只要编译器提供这个功能,就不要再自己去找麻烦。
关于dynamic_cast最有趣的事情在于,主流的C++编译器为了满足一些吝啬的C程序员的要求,一般都提供了把RTTI关掉的编译选项,这样确实可以减少一些空间开销。而如果你使用了这样的编译选项,而你的程序中又使用了dynamic_cast,啊哈,美妙的segment fault立即就会向你袭来。
在不少mail-list中都可以看到有人在抱怨dynamic_cast引起的程序崩溃,其中一些使用g++的提问者还明白地知道自己加上了-fno-rtti的选项,回答问题的老大们就往往会用"foolish"来形容这样的行径。至于用Visual C++的人就更糟糕了,VC6中的cl编译器默认设定是关掉RTTI 的,你必须自己在project settings页面里把它选上,或者手工添加"/GR"选项。在Windows下面试图移植OpenH323类库的同道们,估计肯定有人吃过这个苦头。
最无辜的程序员是这样的,他认为使用ANSI C++标准中定义的东西怎么会有问题呢?但偏偏他遇到了一位热心的Build Master,为了优化性能把RTTI选项给关掉了。我想这个问题,是衡量一个软件开发队伍配置管理水平的一个经典题目。