条款11:以类型安全的方式创建资源和查询接口
更多条款请前往原文出处:http://blog.csdn.net/liuchang5
下面这种写法在COM组件编写过程中这种错误的写法并不少见:
void func()
{
IX *pIX = NULL;
hrRetCode = CoCreateInstance(
CLSID_MYCOMPONENT,
NULL,
CLSCTX_INPROC_SERVER,
IID_IX
(void **)pIX //额~~阴沟里翻船了~
);
assert(hrRetCode);
pIX->IxFunction();
}
上述代码会发生什么?其行为不确定,而且在大多数情况下是错误的。原因是由于创建COM组件或者接口查询的时候使用的函数并非类型安全的。
针对这种情况,你可能会想到我们应该多利用智能指针来避免这一问题。因为智能指针无法强制转换成(void**)类型。貌似这样做能使得你的类型变得安全一些,然而错误还是发生了:
void func()
{
CComPtr<IX> spIX = NULL;
hrRetCode = CoCreateInstance(
CLSID_MYCOMPONENT,
NULL,
CLSCTX_INPROC_SERVER,
IID_IY //额~~这里又出错了 :(
(void **)&pIX
);
assert(hrRetCode);
pIX->IxFunction();
}
好在后面的断言能将错误及时的反馈上来,但是如果我们有更好的方法来避免这一问题的出现,为何不用呢?
解决这一问题的最好方式是用智能指针所提供的接口查询方法:
void func()
{
CComPtr<IX> spIX = NULL; //IID在智能指针创建的时候与其绑定在一起
spIX .CoCreateInstance(CLSID_MYCOMPONENT);
assert(spIX );
pIX->IxFunction();
}
是的,这一点没错。我们推荐使用智能指针,但是我们更多的希望的是你通过智能指针提供的功能性函数来完成资源的创建与查询。他不仅带来了了代码上的简洁,而且使得你的代码在类型上更加的安全。
有时候,我们在考虑设计某个类时,通常会考虑他的移植性和兼容性。这导致我们会慎重的采用一些与编译器相关的特性时,往往采用了谨慎的态度。或者更多时候,我们避讳编译器给我们带来的某些特性,来追求可移植和不同平台下的兼容性。
但值得注意的是,在考虑这些问题之前,我们先应该考虑的是程序的正确执行。一个允许错误肆意存在的程序,即便是可以随意移植,意义也不会很大。
首先看一眼下面这套接口和GUID的定义:
// {994D80AC-A5B1-430a-A3E9-2533100B87CE}
DEFINE_GUID(IID_ICALCULATOR,
0x994d80ac, 0xa5b1, 0x430a, 0xa3, 0xe9, 0x25, 0x33, 0x10, 0xb, 0x87, 0xce);
class ICalculator public : IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE Add(
const int nNum1,
const int nNum2,
int *pnSum
) const = 0;
virtual HRESULT STDMETHODCALLTYPE Sub(
const int nMinuend,
const int nSubtrahend,
int *pnQuotient
) const = 0;
};
这样做没有问题,而且在使用某些智能指针的时候也不会存在问题。如_com_ptr_t允许你在特异化一个智能指针的时候采用如下这种方式将IID和接口绑定起来。
//特异化一个智能指针的时候采用如下这种方式将IID和接口绑定起来
_COM_SMARTPTR_TYPEDEF(ICalculator, IID_ICALCULATOR);
HRESULT Calculaltor()
{
ICalculatorPtr spCalculator (CLSID_CALCULATOR); //构造函数可创建COM组件
int nSum = 0;
spCalculator->Add(1, 2, &nSum);
return S_OK;
}
采用这种方式即便是在特异化时候,不小心将IID和对应ICalculator那么查找和修改起来也会相对于简单一点。而且智能指针一经声明,则将接口指针和IID就绑定在了一起。后续开发便不需要考虑他们之间匹配的问题。
但是如果上述接口声明和CComPtr配合使用,情况就大为不一样了。你的程序可能根本无法通过编译:
void func(void)
{
CComPtr<ICalculator> pCalculator = NULL;
//编译失败,提示:no GUID has been associated with this object
hrRetCode = pCalculator .CoCreateInstance(CLSID_CALCULATOR,);
assert(hrRetCode);
spCalculator->DoSomething();
}
可以看出CComPtr 和 _com_ptr_t 采用了两种不同的方式解决IID和接口的绑定问题。CComPtr需要开发人员在接口声明的时候将IID与接口绑定。而_com_ptr_t则可以根据需要决定是否使用__uuid。
因此为了兼容这两种智能指针我们需要在定义的时候使用如下这种方式:
//使用编译器为我们提供的安全机制指定IID
interface __declspec(uuid("994D80AC-A5B1-430a-A3E9-2533100B87CE")) ICalculator :
IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Add(
const int nNum1,
const int nNum2,
int *pnSum
) const = 0;
virtual HRESULT STDMETHODCALLTYPE Sub(
const int nMinuend,
const int nSubtrahend,
int *pnQuotient
) const = 0;
};
如果你懂得IDL,那么用IDL定义接口并将IID与其绑定会更加合理一些。但暂且让我们这样做,它会使示例想表达的东西更加明确和清晰。
是的,这样虽然移植性稍差一点,但是它使得接口使用起来更为安全和方便。你不需要在每次查询接口和创建COM组件的过程中为接口指定相应的IID。那常常是导致错误的地方。我们需要的是先考虑如何编写难以发生错误的安全代码,之后才是其兼容性。
如果你想了解如何解决uuid和__uuidof所带来的移植和兼容性的讨论。看以参看条款25。