条款25:思考兼容取地址操作符带来的若干问题
智能指针真的很神奇,他能让我们顺利完成如下这种操作:
CComPtr<ICalculator> pCalculator = NULL;
int nSum = 0;
CoCreateInstance(
CLSID_CALCULATOR,
NULL,
CLSCTX_INPROC_SERVER,
IID_ICALCULATOR,
(void **)&spCalculator //很神奇的取地址符
);
spCalculator->DoSomething();
为了支持这种操作CComPtr 和 _com_ptr_t做了不同的实现。首先来看一下CComPtr的实现:
//CComPtr的实现
IUnknown** operator&()
{
ATLASSERT(p==NULL);
return &p;
}
这个操作简洁到让人吃惊,如果省略掉ASSERT语句,整个运算符只是简单的返回了接口指针的地址。但_com_ptr_t的实现却大为不同了:
//_com_ptr_t的实现
Interface** operator&() throw()
{
_Release(); //这个函数会将原来指针所指向的资源释放掉
m_pInterface = NULL;
return &m_pInterface;
}
我们来探究一下这里面的原因,先假设我们的程序中有如下一个函数:
void f1(/*[out]*/IUnknown **ppunk)
{
*ppunk = new CSomeClass; //将其传出
(*ppunk)->AddRef();
}
很明显上述ppunk是一个传出参数,而非传入。这种函数直接忽略了ppunk的传入值。并将接口指针原有的指向覆盖掉。而项目中也有可能出现如下这种形式的函数,他的二级指针不仅仅作为传出,而且同时也作为传入:
void f2(/*[in][out]*/IUnknown **ppunk)
{
if (*ppunk)
{
(*ppunk)->DoSomething(); //此处从语法上说并不完全正确
(*ppunk)->Release(); //但我指向告诉你我们使用过这个传入的指针。
}
*ppunk = new CSomeClass; //再将其传出
(*ppunk)->AddRef();
}
因此,如果一个函数需要二级指针作为参数,他可能隐含如下几层不同的含义:
1.此参数用于传出[out]。
2.此参数用于传入也做传出[in][out]。
你可能还会说,或许他只传入。这种函数看上去似乎有点傻,但语法上他确实是成立的。他或许是f2这个函数修改维护而又需要保持接口不变性的结果:
void f3(/*[in][out]*/IUnknown **ppunk)
{
if (*ppunk)
{
(*ppunk)->DoSomething(); //此处从语法上说并不完全正确
(*ppunk)->Release(); //但我指向告诉你我们使用过这个传入的指针。
}
}
分析完上述情况,我们发现我们简单重载的取地址运算符可能并不能对号入座。他可能同时面临以上三种情况。但试想一下,如果一个智能指针在取地址操作时释放掉原有的COM资源(_com_ptr_t),那它将不能做传入[in]的参数上。但如果一个智能指针,并不释放掉原有的接口指针而只是简单的将其传出(CComPtr的做法),则可能会无法适用于传出参数[out]之上,因为那可能带来资源泄漏。
介于上述情况复杂,&操作符一般会在指针非空的情况下做一个断言。禁止你返回一个非空的接口指针。这样&操作符就被限定使用在仅仅用于传出的函数参数中。你可能还记得我在本条款之初引入的两个大原则。此时正是“智能”和“指针”发生冲突之时,我们需要优先选用第一条原则“1.它能正确的完成资源管理。[智能]”。CComPtr正是采用的这种做法。
而_com_ptr_t的做法与其说是“智能”到过了头,不如说是过于极端了。确实他保证资源无论在那种情况下都不被泄漏。然而,若将此智能指针取地址后传入到一个传入或传入传出功能的函数参数上。他却将智能指针持有的资源隐式释放了,为程序崩溃埋下了伏笔。如下:
void f(/*[in][out]*/IUnknown **ppunk)
{
if (*ppunk)
{
g_stack.push(*ppunknow); //哦~ 如果用_com_ptr_t这一句永远无法执行到。
}
*ppunk = new CSomeClass; //再将其传出
(*ppunk)->AddRef();
}
IMyInterfacePtr sp(CLSID_MYCOMPONENT);
f(sp);
别忘了&操作符不一定是发生在参数传递过程中或赋值过程中。如下这种方式的使用方式也确实存在。而且是在被广泛使用的STL代码之中。
//Microsoft STL的utility文件中swap的实现方法
template<class _Ty> inline
void swap(_Ty& _Left, _Ty& _Right)
{ // exchange values stored at _Left and _Right
if (&_Left != &_Right) //这里用对传入对象的地址进行比较
{ // different, worth swapping
_Ty _Tmp = _Left;
_Left = _Right;
_Right = _Tmp;
}
}
STL中如果排序的时候要交换元素得值,首先会对传入的变量取地址并比较,从而判断两个对象是否相同。如果相同,认为是同一个元素,就不交换了。试想一下如果将CComPtr和_com_ptr_t的对象放入到STL容器中情况会怎样?
CComPtr触发一个断言,提醒你做了危险的操作,好在没有什么副作用。而_com_ptr_t呢?排序工作无法正常进行,而资源可能已经无声无息的被他释放掉。如果这种情况出现了,你的脑子里充满了一大堆的疑问,满世界找原因。