有效的使用和設計COM智能指針 ——條款5:瞭解_com_ptr_t 設計背後的歷史原因

條款5:瞭解_com_ptr_t 設計背後的歷史原因

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

_com_ptr_t是微軟在VC中的一個專有模版類。它封裝了對IUnknownQueryInterface()AddRef()Release()的操作,並提供自己的一些成員函數從而對COM接口指針進行操作。同時_com_ptr_t還簡化了COM接口對引用計數的操作以及不同接口間的查詢操作。

要使用_com_ptr_t這個智能指針,首先需要用_COM_SMARTPTR_TYPEDEF這個宏來聲明特異化(Specialization)版本的_com_ptr_t 類別。之後則可以使用形如“接口名稱+Ptr”這樣的名稱來定義此種接口類型的智能指針。例如:

_COM_SMARTPTR_TYPEDEF(ICalculator, __uuidof(ICalculator));
_COM_SMARTPTR_TYPEDEF(ICOMDebugger,__uuidof(ICOMDebugger));
HRESULT Calculaltor()
{
    ICOMDebuggerPtr spDebugger = NULL;
    ICalculatorPtr  spCalculator (CLSID_CALCULATOR); //構造函數可創建COM組件
    int nSum = 0;
    spCalculator->Add(1, 2, &nSum);
        
    spDebugger = spCalculator;    //自動調用QueryInterface查詢所需要的接口
    spDebugger->GetRefCount();
    
return S_OK;
}//無需手動調用Release(),接口會在智能指針析構時自動調用Release()。


_COM_SMARTPTR_TYPEDEF這個宏,一般放置於單獨的頭文件中。這樣,只要include了此頭文件的相關文件,都能使用名稱爲“接口名+Ptr”這種類型的智能指針。

這使得_com_ptr_t這套智能指針使用起來相對比較簡單,編寫代碼時不存在一大堆針對模版的類型參數化過程。使用者也感覺不到模版的存在,用類似接口指針的方式即可使用此智能指針。

如果想探究_com_ptr_t這套智能指針的特異化過程是如何完成的,我們可以將特異化時候所用到的_COM_SMARTPTR_TYPEDEF這個宏展開:

typedef _com_ptr_t<_com_IIID<IMyInterface, __uuidof(IMyInterface)>> IMyInterfacePtr;

其中_com_IIID 的原型爲:

template<typename _Interface, const IID* _IID /*= &__uuidof(_Interface)*/> 
class _com_IIID 

可以看出_com_IID這個類模版的功能是對IID和具體的類型進行封裝,並把他們綁定在一起。_com_ptr_t則再會將此_com_IID參數化之後的類型作爲類型參數的實參,從而構造一個特異化版本的智能指針類型。

另外值得一提的是,如果希望使用__uuidof這個vc專用的關鍵字,則需要在接口聲明的時候加上形如:

__declspec(uuid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"))

這樣的語法。如下是ICalculator接口的聲明:

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;
};

_com_ptr_t 中封裝了更多的功能性函數(如可以在構造智能指針的時候創建COM組件),並可以通過賦值運算符進行接口的查詢。或許你會問爲什麼CComPtr不提供類似的操作。這個議題涉及到智能指針設計原則上的問題。我們會在“在設計原則中斟酌取捨”進行深入的討論。

看完_com_ptr_t的一些基礎用法後,讓我們再來設想一種情況:如果我們有一個COM組件,但卻拿不到他的頭文件,那麼在VC中應該如何操作他們呢?或許你認爲拿不到頭文件卻要調用函數的情況不太可能發生,因爲這樣做你的代碼無法通過編譯。但事實是,缺少C/C++頭文件這一現象卻存在於大量的COM組件之中。

這些COM的設計者並非沒有照顧到C/C++的程序員(很大程度上,他們也使用C++開發COM),而是他們使用了一種更好的方法來聲明組件的接口——類型庫。

類型庫,是一種與語言無關、適合於解釋性語言和宏語言使用C++頭文件的等價物【1】。換而言之,C++C語言中,我們的類型聲明都用頭文件來代替,而VBdelphi,則可以通過類型庫來完成。

微軟爲VC提供的#import預處理命令,它能將一個類型庫轉換成等價的C/C++頭文件。這樣,開發者只需要發佈一套類型庫,則能在多種語言中定義出相應的接口了。

我們先可以用#import預處理命令來導入一個類型庫,看看編譯器幫我們完成了什麼。我們以ADO爲例,用#import預處理命令導入ADO類型庫的源代碼像是下面這樣的:

#import "C:\Program Files\Common Files\System\ado\msado15.dll"  rename("EOF","rsEOF")

看上去有些複雜,而且和普通編譯預處理命令形式上略有差別。但它卻十分之方便,稍微編譯一下這個程序,則會在相應的目錄下輸出msado15.tlhmsado15.tli兩個文件。

msado15.tlh 包含了接口的聲明,其內容看上去是下面這個樣子的:

// Created by Microsoft (R) C/C++ Compiler Version 12.00.8168.0 (a2f27f36).
//
// d:\...\debug\msado15.tlh
//
// C++ source equivalent of Win32 type library C:\...\ado\msado15.dll
// compiler-generated file created 08/22/11 at 14:19:31 - DO NOT EDIT!
struct __declspec(uuid("00000512-0000-0010-8000-00aa006d2ea4"))
/* dual interface */ _Collection;
struct __declspec(uuid("00000513-0000-0010-8000-00aa006d2ea4"))
/* dual interface */ _DynaCollection;
struct __declspec(uuid("00000534-0000-0010-8000-00aa006d2ea4"))
/* dual interface */ _ADO;
struct __declspec(uuid("00000504-0000-0010-8000-00aa006d2ea4"))
/* dual interface */ Properties;
...
//
// Smart pointer typedef declarations
//
_COM_SMARTPTR_TYPEDEF(_Collection, __uuidof(_Collection)); //哦~ 太眼熟了! 
_COM_SMARTPTR_TYPEDEF(_DynaCollection, __uuidof(_DynaCollection));
_COM_SMARTPTR_TYPEDEF(_ADO, __uuidof(_ADO));
_COM_SMARTPTR_TYPEDEF(Properties, __uuidof(Properties));
_COM_SMARTPTR_TYPEDEF(Property, __uuidof(Property));
_COM_SMARTPTR_TYPEDEF(Error, __uuidof(Error));
_COM_SMARTPTR_TYPEDEF(Errors, __uuidof(Errors));
_COM_SMARTPTR_TYPEDEF(Command15, __uuidof(Command15));
...


msado15.tli包含了接口的實現:

// Created by Microsoft (R) C/C++ Compiler Version 12.00.8168.0 (a2f27f36).
//
// d:\....\debug\msado15.tli
//
// Wrapper implementations for Win32 type library C:\....\ado\msado15.dll
// compiler-generated file created 08/22/11 at 14:19:31 - DO NOT EDIT!
// interface _Collection wrapper method implementations
#pragma implementation_key(1)
inline long _Collection::GetCount ( ) {
    long _result;
    HRESULT _hr = get_Count(&_result);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _result;
}
#pragma implementation_key(2)
inline IUnknownPtr _Collection::_NewEnum ( ) {
    IUnknown * _result;
    HRESULT _hr = raw__NewEnum(&_result);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return IUnknownPtr(_result, false);
}
...

微軟並不希望你去讀懂這兩套文件,也更不指望你去修改他們。註釋中大些的“DO NOT EDIT!”肯定會讓你打消這個念頭。但是從msado15.tlh中你肯定發現如此親切且熟悉的語句了:

//
// Smart pointer typedef declarations
//
_COM_SMARTPTR_TYPEDEF(_Collection, __uuidof(_Collection)); //哦~ 太眼熟了! 
_COM_SMARTPTR_TYPEDEF(_DynaCollection, __uuidof(_DynaCollection));
_COM_SMARTPTR_TYPEDEF(_ADO, __uuidof(_ADO));

這個預處理命令竟然用類型庫生成了_com_ptr_t的智能指針代碼!如果你忘記了_COM_SMARTPTR_TYPEDEF是如何特異化一套智能指針的過程,請回顧一下條款2。這種將某個編譯預處理命令與其特定功能的代碼綁定到一起的行爲,確實很少見。因此你也別指望#import是可移植的,事實上COM組件也無法移植到其他平臺上去。

但你似乎潛在的感覺到了,COM_com_ptr_t和編譯器(應該是編譯器的預處理器)存在與某種關聯。確實如此,微軟在提出COM之後,對VC編譯器加入的對COM的支持。而VBdelphijavascript則更是在語法層面上支持COM(事實上,他們都有一個支持COM的運行時,用以支持COM的這些特性【8】),在那裏沒有智能指針這一說。指向COM接口的變量即爲智能指針。不如讓我們來看一看一段VB代碼。他或許會讓我們更好的理解_com_ptr_t這套智能指針:

dim objVar as MyClass
set objVar = new MyOtherClass
objVar.DoSomething

我的VB功底實在不怎麼好,但上面幾行代碼足以讓一個COM組件工作。我們進一步刨析一下它的運行過程:

1.首先它定義了一個名爲objVar 的變量,類型爲myClass。

2.實例化一個MyOtherClass的COM組件,並且將其賦值到objVar 之上。

3.objVar執行相應的DoSomething函數。

你或會問,第二步中set objVar = new MyOtherClass等號左右兩邊類型是有父子關係嗎?如果沒有,那VB編譯器還會允許它通過編譯?

VBMyClass 與 MyOtherClass確實不需要有任何關係,其實只要MyOtherClass背後隱藏的組件實現了MyClass 着這種類型的接口,那麼程序將正確的工作下去。如果,不支持呢?那他會拋出一個運行時的異常,等待程序員去處理它。

如果這種弱類型的語言影響你的閱讀,你不妨將objVar視作是_com_ptr_t的一個實例。然後我們稍微用C++的語法重新實現以上過程,看看發生了什麼。

_COM_SMARTPTR_TYPEDEF(MyClass, __uuidof(MyClass));
_COM_SMARTPTR_TYPEDEF(MyOtherClass, __uuidof(MyOtherClass));
MyClassPtr spMyClass = NULL;   //dim objVar as MyClass
MyOtherClassPtr spMyOtherClass(CLSID_MYOTHERCLASS); 
spMyClass = spMyOtherClass;     //set objVar = new MyOtherClass
spMyClass.DoSomething();       //objVar.DoSomething

你會發現,通過_com_ptr_t操作COM接口的方法和VB中使用變量操作接口的方式驚人的相似。形如“spMyClass = spMyOtherClass;”這樣不同類型接口的查詢操作在VC中通過_com_ptr_t對賦值運算符的重載而實現了。若查詢接口失敗,同樣是拋出一個運行時的異常。

由於VC缺少對COM必要的運行時【8】,_com_ptr_t的設計者可能在將COM技術用於VC之中時,做了如下考慮:

1.如果VB能夠兼容的東西,VC也要能使用。因此#import的出現使得VC通過_com_ptr_t方便的導入類型庫。

2.VB採用的接口查詢和使用方式VC也應當可以採用。因此_com_ptr_t重載了賦值運算符來查詢接口。重載多種構造函數用以像VB那樣創建對象。

3.VB所表現出現了的特點VC也應當以相同的方式表現出來。因此接口查詢時候出現錯誤,_com_ptr_t會如同VB一樣拋出一個異常。

似乎它就是爲了能夠與VB或者Delphi以相似的語法或機制來操作COM接口而存在的。因此他在很多情況下有違C/C++的約定(如它可能會在賦值運算符中拋出一個異常)。但這種特性可以使得代碼更加容易被複用,學習智能指針的時間也得意縮短。

_com_ptr_t的存在使得不同語言操作COM接口的方式得到了統一。他的設計複雜,功能強大。使得VC可以與其他語言一樣方便的使用類型庫。當然追求這種統一性也使得他暴露出了相當多的問題(如條款7中自動接口查詢帶來的風險)。

但不管它如何,此時你知道了它的設計意圖。這會幫助你理解這套智能指針的其他細節。

發佈了28 篇原創文章 · 獲贊 72 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章