條款11:以類型安全的方式創建資源和查詢接口
下面這種寫法在COM組件編寫過程中這種錯誤的寫法並不少見:
view plaincopy to clipboardprint?void func()
{
IX *pIX = NULL;
hrRetCode = CoCreateInstance(
CLSID_MYCOMPONENT,
NULL,
CLSCTX_INPROC_SERVER,
IID_IX
(void **)pIX //額~~陰溝裡翻船了~
);
assert(hrRetCode);
pIX->IxFunction();
}
void func()
{
IX *pIX = NULL;
hrRetCode = CoCreateInstance(
CLSID_MYCOMPONENT,
NULL,
CLSCTX_INPROC_SERVER,
IID_IX
(void **)pIX //額~~陰溝裡翻船了~
);
assert(hrRetCode);
pIX->IxFunction();
}
上述代碼會發生什麼?其行為不確定,而且在大多數情況下是錯誤的。原因是由於創建COM組件或者接口查詢的時候使用的函數並非類型安全的。
針對這種情況,你可能會想到我們應該多利用智能指針來避免這一問題。因為智能指針無法強制轉換成(void**)類型。貌似這樣做能使得你的類型變得安全一些,然而錯誤還是發生了:
view plaincopy to clipboardprint?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;
hrRetCode = CoCreateInstance(
CLSID_MYCOMPONENT,
NULL,
CLSCTX_INPROC_SERVER,
IID_IY //額~~這裡又出錯了 :(
(void **)&pIX
);
assert(hrRetCode);
pIX->IxFunction();
}
好在後面的斷言能將錯誤及時的反饋上來,但是如果我們有更好的方法來避免這一問題的出現,為何不用呢?
解決這一問題的最好方式是用智能指針所提供的接口查詢方法:
view plaincopy to clipboardprint?void func()
{
CComPtr<IX> spIX = NULL; //IID在智能指針創建的時候與其綁定在一起
spIX .CoCreateInstance(CLSID_MYCOMPONENT);
assert(spIX );
pIX->IxFunction();
}
void func()
{
CComPtr<IX> spIX = NULL; //IID在智能指針創建的時候與其綁定在一起
spIX .CoCreateInstance(CLSID_MYCOMPONENT);
assert(spIX );
pIX->IxFunction();
}
是的,這一點沒錯。我們推薦使用智能指針,但是我們更多的希望的是你通過智能指針提供的功能性函數來完成資源的創建與查詢。他不僅帶來了了代碼上的簡潔,而且使得你的代碼在類型上更加的安全。
有時候,我們在考慮設計某個類時,通常會考慮他的移植性和兼容性。這導致我們會慎重的采用一些與編譯器相關的特性時,往往采用了謹慎的態度。或者更多時候,我們避諱編譯器給我們帶來的某些特性,來追求可移植和不同平台下的兼容性。
但值得注意的是,在考慮這些問題之前,我們先應該考慮的是程序的正確執行。一個允許錯誤肆意存在的程序,即便是可以隨意移植,意義也不會很大。
首先看一眼下面這套接口和GUID的定義:
view plaincopy to clipboardprint?// {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;
};
// {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和接口綁定起來。
view plaincopy to clipboardprint?//特異化一個智能指針的時候采用如下這種方式將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和接口綁定起來
_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配合使用,情況就大為不一樣了。你的程序可能根本無法通過編譯:
view plaincopy to clipboardprint?void func(void)
{
CComPtr<ICalculator> pCalculator = NULL;
//編譯失敗,提示:no GUID has been associated with this object
hrRetCode = pCalculator .CoCreateInstance(CLSID_CALCULATOR,);
assert(hrRetCode);
spCalculator->DoSomething();
}
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。
因此為了兼容這兩種智能指針我們需要在定義的時候使用如下這種方式:
view plaincopy to clipboardprint?//使用編譯器為我們提供的安全機制指定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;
};
//使用編譯器為我們提供的安全機制指定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所帶來的移植和兼容性的討論。
作者“liuchang5的專欄”