19.8 comstl::interface_cast
尽管保持书中的示例的非专有性是件好事,但我觉得现在是提出一些特定的技术的时候了。如果你没有听过COM的话,我会对那些跟本节密切相关的东西作一点简短的介绍(如果你想要更深入地了解COM,你可以参考这方面的一些优秀的书籍[Box1998, Broc1995, Eddo1998],捋起袖子开始学习吧!
COM的含义是组件对象模型(Component Object Model)。至于它到底是做什么用的,在它的名字中已经表现得很明显了:它是一个用于描述并实现组件软件、创建并管理组件对象的模型。COM是基于IUnknow接口的,该接口包含3个方法:AddRef()和Release()负责引用计数,QueryInterface()则用于询问某个COM对象是否具有另外一个"性质",即我们所要查询的"性质",并请求获取一个指向该"性质"的指针。这里我们所谈论的"性质"即派生自IUnknown接口的其他接口,每一个接口由惟一的接口标识符(IID,unique interface identifiers)进行标识,后者是一个128位的数。
程序清单19.13
- interface IImpCpp
- : public IUnknown
- {
- virtual HRESULT CanSupportOO(BOOL *bOO) = 0;
- };
- extern const IID IID_IImpCpp;
- interface IImpC
- : public IUnknown
- {
- virtual HRESULT CanSupportOO(BOOL *bOO) = 0;
- };
- extern const IID IID_IImpC;
由于一个COM对象可以实现若干接口,而且COM并没有规定该对象必须继承自它所实现的这些接口中的任一个或全部,所以我们平时对其他基于继承的类型所进行的强制,到了COM接口上可能就不再适用了。比如说,如果我们持有一个指向IImpCpp接口的指针并希望将它转换为IImpC,我们就不能对它进行强制,因为它们并不是直接相关的,而只不过是共享同一个父亲(即IUnknown)而已。(关于COM,另外还有其他一些重要的规则使得我们无法将dynamic_cast应用在这种横向强制(cross-cast)[Stro1997]上。)事实上,你可以从上面这个简单的例子中看出,如果我们希望同时从IImpCpp和IImpC继承,我们就得花上一点心思来为它们的CanSuportOO()方法分别提供不同的实现。
因此,如果我们想要访问一个COM对象的另一个"性质",我们就必须向当前接口请求或查询该COM对象是否实现了我们所需要的接口。像这样:
程序清单19.14
- IImpC *ic = . . .;
- IImpCpp *icpp;
-
- // 请求一个指向IImpC接口的指针
- HRESULT hr = ic->QueryInterface(IID_IImpCpp,
- reinterpret_cast<void**>(&icpp));
- if(SUCCEEDED(hr))
- {
- BOOL bOO;
- icpp->CanSupportOO(&bOO);
- icpp->Release(); // 释放接口
- }
以上的代码近乎样板,我们平常从一个现有接口获取另一个接口的指针时编写的代码形式大抵如此。但该机制存在两个问题。首先,人们必须确保给出了正确的IID常量。其次,代码中的那个reinterpret_cast强制太容易写错了,常见的错误是忘记"&"操作符,即错写成了reinterpret_cast<void**>(icpp)。这种错误只有在彻底的代码覆盖测试下才会暴露出来,然而在大项目中很难做到彻底的代码覆盖测试[Glas2003]。
有很多人曾试图把这段过程封装起来,其中有些人使用了"智能"指针,但效果差强人意--至少"差强我意"。但我深知没有人会喜欢一个只会吹毛求疵而自己又拿不出像样的替代方案的"批评家"的,所以现在我斗胆拿出我自己的解决方案:接口强制。