條款5:了解_com_ptr_t 設計背後的歷史原因
_com_ptr_t是微軟在VC中的一個專有模版類。它封裝了對IUnknown的QueryInterface()、AddRef()和Release()的操作,並提供自己的一些成員函數從而對COM接口指針進行操作。同時_com_ptr_t還簡化了COM接口對引用計數的操作以及不同接口間的查詢操作。
要使用_com_ptr_t這個智能指針,首先需要用_COM_SMARTPTR_TYPEDEF這個宏來聲明特異化(Specialization)版本的_com_ptr_t 類別。之後則可以使用形如“接口名稱+Ptr”這樣的名稱來定義此種接口類型的智能指針。例如:
view plaincopy to clipboardprint?_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(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這個宏展開:
view plaincopy to clipboardprint?typedef _com_ptr_t<_com_IIID<IMyInterface, __uuidof(IMyInterface)>> IMyInterfacePtr;
typedef _com_ptr_t<_com_IIID<IMyInterface, __uuidof(IMyInterface)>> IMyInterfacePtr;
其中_com_IIID 的原型為:
view plaincopy to clipboardprint?template<typename _Interface, const IID* _IID /*= &__uuidof(_Interface)*/>
class _com_IIID
template<typename _Interface, const IID* _IID /*= &__uuidof(_Interface)*/>
class _com_IIID
可以看出_com_IID這個類模版的功能是對IID和具體的類型進行封裝,並把他們綁定在一起。_com_ptr_t則再會將此_com_IID參數化之後的類型作為類型參數的實參,從而構造一個特異化版本的智能指針類型。
另外值得一提的是,如果希望使用__uuidof這個vc專用的關鍵字,則需要在接口聲明的時候加上形如:
view plaincopy to clipboardprint?__declspec(uuid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"))
__declspec(uuid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"))
這樣的語法。如下是ICalculator接口的聲明:
view plaincopy to clipboardprint?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;
};
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語言中,我們的類型聲明都用頭文件來代替,而VB、delphi,則可以通過類型庫來完成。
微軟為VC提供的#import預處理命令,它能將一個類型庫轉換成等價的C/C++頭文件。這樣,開發者只需要發布一套類型庫,則能在多種語言中定義出相應的接口了。
我們先可以用#import預處理命令來導入一個類型庫,看看編譯器幫我們完成了什麼。我們以ADO為例,用#import預處理命令導入ADO類型庫的源代碼像是下面這樣的:
view plaincopy to clipboardprint?#import "C:\Program Files\Common Files\System\ado\msado15.dll" rename("EOF","rsEOF")
#import "C:\Program Files\Common Files\System\ado\msado15.dll" rename("EOF","rsEOF")
看上去有些復雜,而且和普通編譯預處理命令形式上略有差別。但它卻十分之方便,稍微編譯一下這個程序,則會在相應的目錄下輸出msado15.tlh和msado15.tli兩個文件。
msado15.tlh 包含了接口的聲明,其內容看上去是下面這個樣子的:
view plaincopy to clipboardprint?// 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));
...
// 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包含了接口的實現:
view plaincopy to clipboardprint?// 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);
}
...
// 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中你肯定發現如此親切且熟悉的語句了:
view plaincopy to clipboardprint?//
// Smart pointer typedef declarations
//
_COM_SMARTPTR_TYPEDEF(_Collection, __uuidof(_Collection)); //哦~ 太眼熟了!
_COM_SMARTPTR_TYPEDEF(_DynaCollection, __uuidof(_DynaCollection));
_COM_SMARTPTR_TYPEDEF(_ADO, __uuidof(_ADO));
//
// 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的支持。而VB、delphi、javascript則更是在語法層面上支持COM(事實上,他們都有一個支持COM的運行時,用以支持COM的這些特性【8】),在那裡沒有智能指針這一說。指向COM接口的變量即為智能指針。不如讓我們來看一看一段VB代碼。他或許會讓我們更好的理解_com_ptr_t這套智能指針:
view plaincopy to clipboardprint?dim objVar as MyClass
set objVar = new MyOtherClass
objVar.DoSomething
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編譯器還會允許它通過編譯?
在VB中MyClass 與 MyOtherClass確實不需要有任何關系,其實只要MyOtherClass背後隱藏的組件實現了MyClass 著這種類型的接口,那麼程序將正確的工作下去。如果,不支持呢?那他會拋出一個運行時的異常,等待程序員去處理它。
如果這種弱類型的語言影響你的閱讀,你不妨將objVar視作是_com_ptr_t的一個實例。然後我們稍微用C++的語法重新實現以上過程,看看發生了什麼。
view plaincopy to clipboardprint?_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_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中自動接口查詢帶來的風險)。
但不管它如何,此時你知道了它的設計意圖。這會幫助你理解這套智能指針的其他細節。
作者“liuchang5的專欄”