条款6:尽量以智能指针替换接口指针
更多条款请前往原文出处:http://blog.youkuaiyun.com/liuchang5
如一个程序员要使用IHello接口的指针需要书写如下代码。
void SomeApp( IHello * pHello )
{
IHello* pCopy = pHello;
pCopy->AddRef();
OtherApp();
pCopy->Hello();
pCopy->Release();
}
这样的代码看上去并没有太多问题,但是如果将异常考虑在内的话,上面代码就不那么乐观了。
假设OtherApp()中抛出了异常,那么pCopy->Release()将永远无法被执行到。COM组件无法释放,资源泄露也便产生了。有些公司规定代码中不允许出现异常,但即便是代码中不存在任何throw语句的情况下要消灭代码中所有的异常还是一件非常困难的事情。原因有如下:
1.你不能保证第三方类库中不抛出异常。或者你会话费相当大的时间来阅读文档已确定它不会抛出异常。
2.C++语言的某些操作默认会抛出异常。如默认的流操作,值传递中的动态申请内存,或者是动态的下转型dynamic_cast操作。
针对以上两点,我要补充说明的是消除异常是很困难的一件事情,而绝非不可能。但是这些问题总是在我们关注的时候被很好的解决,比如你可以要求不能使用流操作或者在new关键字前加nothrow来禁止这些异常的出现,甚至是更改编译器选项完全禁止异常。但若是出现了疏忽,异常便成了一个无法回避的问题。
或许你会继续争论将异常消灭的其他办法。但是事实你仅仅只是为了消灭吊异常所带来的副作用,而非其本生。在你拿出更好的办法前让我们先来看看智能指针如何解决这一问题:
void SomeApp( IHello * pHello )
{
CComPtr<IHello> spHello= pHello;
OtherApp();
spHello->Hello();
}
CComPtr的用法很简单,它的详细介绍我们会在下一节中进行说明。而现在你只需要知道我们是怎么使用的即可。在这里,以IHello*为例,将程序中所有接口指针类型(除了参数),都使用CComPtr<IHello> 代替即可。即程序中除了参数之外,再也不要使用IHello*,全部以CComPtr<IHello>代替。或许这么做不会使给你程序带来太大的麻烦,但是他的收益却非常之大。
首先讨厌的AddRef()和Release()操作消失了。与其说消失,不如说智能指针帮我们在适当的时候处理了。这样做使得代码行数缩短,逻辑清晰的体现出来。
再回到异常问题上来,上述代码中若是OtherApp(),再抛出异常呢?此时的过程是,若OtherApp()抛出一个异常,则智能指针从所在函数中出栈,spHello被析构,CComPtr类型对象在析构过程中会自动调用Release()函数减少COM的引用计数,从而避免资源的泄漏。
这样看来,一切问题都优雅的解决了。或许你也不会再把心思画在消除异常所带来的副作用这类舍本求末的问题上了。
可能你已经初步感觉绝到智能指针带来了的一些便利之处,但如果这些优势还不足以说服你的话,或许下面这个例子会进一步改变你的想法:
IUnknown *PIUnknown = CreateInstance();
IX *pIX = NULL;
HRESULT hr = pIUnknown->QueryInterface(IID_IX, (void **)&pIX);
If (SUCCEEDED(hr))
{
pIX->Fx(); //这里开始主要的逻辑部分。
IX *pIX2 = pIX; //但引用计数操作却占据了这部分代码的一半。
pIX2->AddRef();
pIX2->Fx();
pIX2->Release();
pIX->Release();
}
上述代码中,为了满足引用计数的三条规则,pIX赋值给pIX2的时候调用了AddRef()。但实际上,由于pIX2的生命周期与pIX1相同,所以没有必要对pIX2使用AddRef()和Release()。这些冗余的代码使得代码的可读性大大降低。
同时,若程序中对接口指针赋值操作过多,也会导致由于程序员遗漏AddRef()与Release()操作,而造成灾难性的后果。并且这种错误的调试过程十分麻烦。
看看智能指针是如何解决这一问题的:
CComPtr<IX> spIX = NULL;
HRESULT hr = spIX.CoCreateInstance(CLSID_MYCOMPONENT);
If(SUCCEEDED(hr))
{
spIX->Fx(); //这里开始主要的逻辑部分。
CComPtr<IX> spIX2 = spIX; //有了智能指针,就只剩下逻辑了。:)
spIX2->Fx();
}
如果这还不够麻烦的话,那看看下面这个例子【5】。看完这个例子你可能会对于智能指针的使用更加渴望。
void f(void) {
IUnknown *rgpUnk[3];
HRESULT hr = GetObject(&rgpUnk[0]);
if (SUCCEEDED(hr)) {
hr = GetObject(&rgpUnk [1]); //为了使得代码简单这里用GetObject代替QueryInterface
if (SUCCEEDED(hr)) {
hr = GetObject(&rgpUnk[2]);
if (SUCCEEDED(hr)) {
UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
rgpUnk[2]->Release();
}
rgpUnk[1]->Release();
}
rgpUnk[0]->Release();
}
}
我并不觉得你能一眼看清楚上面代码的关键所在。只有当你一行一行读下来,你才会恍然大雾“原来只是为了调用UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2])这个函数”。而他需要的是三个COM接口指针。于是出现了这种层层嵌套的代码,以及嵌套之后的Release调用。你或许会用大量的精力来考虑括号是否配对,Release和GetObject是否成对出现。或许在它还使得你不得不拖动IDE下方或者右侧的滚动条来查看后续代码。
他不仅让人眼花,更重要的是他找不到关键逻辑代码。智能指针能简化这个编写过程,而且十分优雅:
void f(void) {
CComPtr<IUnknown> rgpUnk[3];
if (FAILED(GetObject(&rgpUnk[0]))) return;
if (FAILED(GetObject(&rgpUnk[1]))) return;
if (FAILED(GetObject(&rgpUnk[2]))) return;
UseObjects(rgpUnk[0], rgpUnk[1], rgpUnk[2]);
}
少了多余的AddRef()和Release(),世界清静了,你看到了UserObjects这个关键的逻辑,
或许你现在已经对智能指针跃跃欲试,因为它可以获取如此多的好处,而代价却相当之少(它只需要在函数堆栈上开辟一个极小的空间用于存放智能指针对象,大小往往也和普通指针的大小相同)。但在此之前,我们来看一些更加令人兴奋的特性。
观察下面代码:
HRESULT hrRetCode = E_FAIL;
IX *pIX = NULL;
hrRetCode = CoCreateInstance(
CLSID_MYCOMPONENT,
NULL,
CLSCTX_INPROC_SERVER,
IID_IY, //哦~ 真悲剧,传错了IID。
(void **)&pIX
);
KG_COM_ASSERT_EXIT(hrRetCode);
pIX->fun();
如果你仔细查看便会发现,查询接口的时候用IID_IY却用IX类型的指针作为参数接收。类似的错误还有可能是你查询的是IX但是用的是IY的接口进行接收。对于这样的错误代码,执行之后会发生什么,这实在没有什么值得我们深入研究的必要。而我们考虑得更多的应该是研究避免这一问题的方法。
首先来探究一下上述错误原因的关键:
1.IID 和接口类型没有静态的绑定在一起,这可能导致IID和接口的错误搭配。
2.CoCreateInstance的传出参数(最后一个参数)是void**类型,因此他是类型不安全的,完全有可能将任意类型的接口错误传入。
解决问题的办法仍然是智能指针。看一下下面这个优雅的方案,类型安全的问题似乎可以得到解决。
HRESULT hrRetCode = E_FAIL;
CComPtr<IX> spIX
hrRetCode = spIX.CoCreateInstance(CLSID_MYCOMPONENT);//不存在IID和void**了
KG_COM_ASSERT_EXIT(hrRetCode);
pIX->fun();
以上代码中在智能指针后加“.”的用法貌似会让你对“指针”这个概念产生疑惑。你可能会问它不应该是->操作符吗?我们会在后面章节的讨论中涉及这个问题。暂且你不妨将智能指针理解为一个资源管理对象,而这个对象填充了一个安全创建COM组件的方法。类似的操作还存在智能指针对于QueryInterface这类操作中。
有些智能指针提供给我们一种更为方便的方式来创建COM组件和查询接口。如_com_ptr_t可以如下这种方式创建COM组件:
_COM_SMARTPTR_TYPEDEF(ICalculator, __uuidof(ICalculator));
ICalculatorPtr spIX(CLSID_MYCOMPONENT);
KG_ASSERT_EXIT(spIX);
spIX->fun();
关于上例中_COM_SMARTPTR_TYPEDEF是什么,我们会在后面有详细介绍,暂且读者可以将其视作一个声明。这样一来除了创建COM组件、必要的断言以及函数调用,不存在冗余的代码。对比一下之前的做法,你是否会觉得我们对智能指针的选择和使用有一定的理由了?
需要补充说明的是,我前面所说的是“智能指针提供类型安全的操作”,而并没有说智能指针是绝对类型安全的。这意味着,在智能指针的使用过程中仍然有可能出现类型安全的问题。进一步的讨论请参考“按照规则而不乱用智能指针”。