天天看點

有效的使用和設計COM智能指針——條款11:以類型安全的方式建立資源和查詢接口

條款11:以類型安全的方式建立資源和查詢接口

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

下面這種寫法在COM元件編寫過程中這種錯誤的寫法并不少見:

void func()
{
    IX *pIX  = NULL;
    hrRetCode = CoCreateInstance( 
        CLSID_MYCOMPONENT,
        NULL,
        CLSCTX_INPROC_SERVER,
        IID_IX 
        (void **)pIX         //額~~陰溝裡翻船了~
    );
    assert(hrRetCode);
    pIX->IxFunction();
}                             
           

上述代碼會發生什麼?其行為不确定,而且在大多數情況下是錯誤的。原因是由于建立COM元件或者接口查詢的時候使用的函數并非類型安全的。

針對這種情況,你可能會想到我們應該多利用智能指針來避免這一問題。因為智能指針無法強制轉換成(void**)類型。貌似這樣做能使得你的類型變得安全一些,然而錯誤還是發生了:

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;    //IID在智能指針建立的時候與其綁定在一起
    spIX .CoCreateInstance(CLSID_MYCOMPONENT);
    assert(spIX );
    pIX->IxFunction();
}                             
           

是的,這一點沒錯。我們推薦使用智能指針,但是我們更多的希望的是你通過智能指針提供的功能性函數來完成資源的建立與查詢。他不僅帶來了了代碼上的簡潔,而且使得你的代碼在類型上更加的安全。

有時候,我們在考慮設計某個類時,通常會考慮他的移植性和相容性。這導緻我們會慎重的采用一些與編譯器相關的特性時,往往采用了謹慎的态度。或者更多時候,我們避諱編譯器給我們帶來的某些特性,來追求可移植和不同平台下的相容性。

但值得注意的是,在考慮這些問題之前,我們先應該考慮的是程式的正确執行。一個允許錯誤肆意存在的程式,即便是可以随意移植,意義也不會很大。

首先看一眼下面這套接口和GUID的定義:

// {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和接口綁定起來。

//特異化一個智能指針的時候采用如下這種方式将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配合使用,情況就大為不一樣了。你的程式可能根本無法通過編譯:

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。

是以為了相容這兩種智能指針我們需要在定義的時候使用如下這種方式:

//使用編譯器為我們提供的安全機制指定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所帶來的移植和相容性的讨論。看以參看條款25。