问题描述:
在 C++ 面向对象编程中,常常会遇到“类型识别”的需求:已知某个对象的基类型指针,需要识别该对象的派生类型,访问派生类型的公有函数。简单说就是,将基类型指针转换为派生类型指针,并用来访问派生类型的公有函数。
问题分析:
“类型识别”本质上是一个从抽象转为具象的过程,破坏了对象抽象性。虽不值得提倡,但在具象化情景下,“类型识别”又是必须的。
“类型识别”常规方法是强制类型转换。这需要先定义一个“对象类型”枚举表,再在基类里定义一个返回“对象类型”的函数。这种方法的缺点显而易见:
1. 需要维护全局性的“对象类型”枚举表;
2. 向基类型构造函数传入“对象类型”参数;
3. 每次“类型转换”前,先要获得“对象类型”值,判断对象类型;
“类型识别”的另一种标准方法是RTTI。作为一项 C++ 编译选项,RTTI 直接将上述常规方法教给编译器做了。然而,这样也是有缺点的:
1. RTTI会引入额外的开销。这是可以理解的,问题是RTTI这种开销会附加到所有类型上去;
2. RTTI要求所有库都以RTTI方式编译,才能正常工作。而现实世界总是复杂的,这一点未必总是能实现;
3. 个人认为RTTI是一种过于形式化的东西,违反了 C++ 的简洁性、高效性原则;
4. 个人认为RTTI的低层形式化的东西,很多时候,我们仍然需要定义不依赖于C++类的“对象类型”枚举表;
阅读 WebKit 代码时,我看到了一种基于虚函数的“类型识别”方法,代码如下:
class EventTarget {
public:
void ref() { refEventTarget(); }
void deref() { derefEventTarget(); }
virtual EventSource* toEventSource();
virtual MessagePort* toMessagePort();
virtual Node* toNode();
virtual DOMWindow* toDOMWindow();
virtual XMLHttpRequest* toXMLHttpRequest();
virtual XMLHttpRequestUpload* toXMLHttpRequestUpload();
在基类中,定义向派生类型的转换函数,并且返回为NULL。每个派生类型,重新实现向自身转换的转换函数,返回自身的指针。我感觉这种方法:
1. 非常安全,使用方便;
2. 效率相对比较高;
3. 省却了全局性的“对象类型”枚举表,却带来了维护基类“转换函数”的负担;
4. 造成了基类对派生类的依赖性,似乎不太好。
在上述方法的基础上,设想了一种改进的方法,设计原则是“从基类型向派生类型转换的函数,应该放置在派生类中,以静态函数的方式定义”。具体方法如下:
1. 基类定义 void* toXXXX() 和 bool isXXXX() 两个私有虚函数,用编程规范规定,禁止派生类直接访问它们。
2. 派生类重定义上述函数。
3. 派生类型定义 XXXX* toType(Base*) 和 bool isType(Base*)两个静态函数,内部实现就是通过上述私有虚函数完成的。
4. 所有的“类型识别操作”都使用上述静态函数完成。
问题小结:
上述四种方式,很难说哪一种方式更好,宜具体情况具体对待。
我本人偏向于,使用最后一种方式作为C++编程规范和模式,因为,它更是一种接口规范,其内部实现可以改为前三种方法中的任何一种。至于它带来的些许工作量,我认为,在它成为一种工作规范后,那些少量代码仅仅只是一点点没有难度的机械性编程,完全可以忽略不计。如果不介意在代码中使用宏机制,那么更可以用宏来实现,那么只需定义编译宏就能就在三种方式之间任意切换。
最后一种方式,体现了一种策略:当你想为基类定义一些与派生类相关的工具函数时,倒不如将其定义为派生类的静态函数,以避免派生类型对基类型的“污染”。