如一個程序員要使用IHello接口的指針需要書寫如下代碼。
view plaincopy to clipboardprint?void SomeApp( IHello * pHello )
{
IHello* pCopy = pHello;
pCopy->AddRef();
OtherApp();
pCopy->Hello();
pCopy->Release();
}
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來禁止這些異常的出現,甚至是更改編譯器選項完全禁止異常。但若是出現了疏忽,異常便成了一個無法回避的問題。
或許你會繼續爭論將異常消滅的其他辦法。但是事實你僅僅只是為了消滅吊異常所帶來的副作用,而非其本生。在你拿出更好的辦法前讓我們先來看看智能指針如何解決這一問題:
view plaincopy to clipboardprint?void SomeApp( IHello * pHello )
{
CComPtr<IHello> spHello= pHello;
OtherApp();
spHello->Hello();
}
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的引用計數,從而避免資源的洩漏。
這樣看來,一切問題都優雅的解決了。或許你也不會再把心思畫在消除異常所帶來的副作用這類捨本求末的問題上了。
可能你已經初步感覺絕到智能指針帶來了的一些便利之處,但如果這些優勢還不足以說服你的話,或許下面這個例子會進一步改變你的想法:
view plaincopy to clipboardprint?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();
}
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()操作,而造成災難性的後果。並且這種錯誤的調試過程十分麻煩。
看看智能指針是如何解決這一問題的:
view plaincopy to clipboardprint?CComPtr<IX> spIX = NULL;
HRESULT hr = spIX.CoCreateInstance(CLSID_MYCOMPONENT);
If(SUCCEEDED(hr))
{
spIX->Fx(); //這裡開始主要的邏輯部分。
CComPtr<IX> spIX2 = spIX; //有了智能指針,就只剩下邏輯了。:)
spIX2->Fx();
}
CComPtr<IX> spIX = NULL;
HRESULT hr = spIX.CoCreateInstance(CLSID_MYCOMPONENT);
If(SUCCEEDED(hr))
{
spIX->Fx(); //這裡開始主要的邏輯部分。
CComPtr<IX> spIX2 = spIX; //有了智能指針,就只剩下邏輯了。:)
spIX2->Fx();
}
如果這還不夠麻煩的話,那看看下面這個例子【5】。看完這個例子你可能會對於智能指針的使用更加渴望。
view plaincopy to clipboardprint?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();
}
}
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下方或者右側的滾動條來查看後續代碼。
他不僅讓人眼花,更重要的是他找不到關鍵邏輯代碼。智能指針能簡化這個編寫過程,而且十分優雅:
view plaincopy to clipboardprint?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]);
}
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這個關鍵的邏輯,
或許你現在已經對智能指針躍躍欲試,因為它可以獲取如此多的好處,而代價卻相當之少(它只需要在函數堆棧上開辟一個極小的空間用於存放智能指針對象,大小往往也和普通指針的大小相同)。但在此之前,我們來看一些更加令人興奮的特性。
觀察下面代碼:
view plaincopy to clipboardprint?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();
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**類型,因此他是類型不安全的,完全有可能將任意類型的接口錯誤傳入。
解決問題的辦法仍然是智能指針。看一下下面這個優雅的方案,類型安全的問題似乎可以得到解決。
view plaincopy to clipboardprint?HRESULT hrRetCode = E_FAIL;
CComPtr<IX> spIX
hrRetCode = spIX.CoCreateInstance(CLSID_MYCOMPONENT);//不存在IID和void**了
KG_COM_ASSERT_EXIT(hrRetCode);
pIX->fun();
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組件:
view plaincopy to clipboardprint?_COM_SMARTPTR_TYPEDEF(ICalculator, __uuidof(ICalculator));
ICalculatorPtr spIX(CLSID_MYCOMPONENT);
KG_ASSERT_EXIT(spIX);
spIX->fun();
_COM_SMARTPTR_TYPEDEF(ICalculator, __uuidof(ICalculator));
ICalculatorPtr spIX(CLSID_MYCOMPONENT);
KG_ASSERT_EXIT(spIX);
spIX->fun();
關於上例中_COM_SMARTPTR_TYPEDEF是什麼,我們會在後面有詳細介紹,暫且讀者可以將其視作一個聲明。這樣一來除了創建COM組件、必要的斷言以及函數調用,不存在冗余的代碼。對比一下之前的做法,你是否會覺得我們對智能指針的選擇和使用有一定的理由了?
需要補充說明的是,我前面所說的是“智能指針提供類型安全的操作”,而並沒有說智能指針是絕對類型安全的。這意味著,在智能指針的使用過程中仍然有可能出現類型安全的問題。進一步的討論請參考“按照規則而不亂用智能指針”。
——條款13:必須提前釋放COM組件時,別妄想智能指針幫你完成
作者“liuchang5的專欄”