条款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”这样的名称来定义此种接口类型的智能指针。例如:
_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语言中,我们的类型声明都用头文件来代替,而VB、delphi,则可以通过类型库来完成。
微软为VC提供的#import预处理命令,它能将一个类型库转换成等价的C/C++头文件。这样,开发者只需要发布一套类型库,则能在多种语言中定义出相应的接口了。
我们先可以用#import预处理命令来导入一个类型库,看看编译器帮我们完成了什么。我们以ADO为例,用#import预处理命令导入ADO类型库的源代码像是下面这样的:
#import "C:\Program Files\Common Files\System\ado\msado15.dll" rename("EOF","rSEOF")
看上去有些复杂,而且和普通编译预处理命令形式上略有差别。但它却十分之方便,稍微编译一下这个程序,则会在相应的目录下输出msado15.tlh和msado15.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,__uuidof(this)); return IUnknownPtr(_result,false); } ...
微软并不希望你去读懂这两套文件,也更不指望你去修改他们。注释中大些的“DONOTEDIT!”肯定会让你打消这个念头。但是从msado15.tlh中你肯定发现如此亲切且熟悉的语句了:
// // Smart pointer typedef declarations // _COM_SMARTPTR_TYPEDEF(_Collection,__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这套智能指针:
dim objVar as MyClass set objVar = new MyOtherClass objVar.DoSomething
我的VB功底实在不怎么好,但上面几行代码足以让一个COM组件工作。我们进一步刨析一下它的运行过程:
1.首先它定义了一个名为objVar的变量,类型为myClass。
2.实例化一个MyOtherClass的COM组件,并且将其赋值到objVar之上。
3.objVar执行相应的DoSomething函数。
你或会问,第二步中setobjVar=newMyOtherClass等号左右两边类型是有父子关系吗?如果没有,那VB编译器还会允许它通过编译?
在VB中MyClass与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中自动接口查询带来的风险)。
但不管它如何,此时你知道了它的设计意图。这会帮助你理解这套智能指针的其他细节。