天天看點

有效的使用和設計COM智能指針——條款25:思考相容取位址操作符帶來的若幹問題

條款25:思考相容取位址操作符帶來的若幹問題

更多條款請前往原文出處:http://blog.csdn.net/liuchang5

智能指針真的很神奇,他能讓我們順利完成如下這種操作:

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呢?排序工作無法正常進行,而資源可能已經無聲無息的被他釋放掉。如果這種情況出現了,你的腦子裡充滿了一大堆的疑問,滿世界找原因。