有效的使用和设计COM智能指针 ——条款5:了解_com_ptr_t 设计背
条款5:了解_com_ptr_t设计背后的历史原因
更多条款请前往原文出处:http://blog.csdn.net/liuchang5
_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 FilesCommon FilesSystemadomsado15.dll" rename("EOF","rSEOF") 看上去有些复杂,而且和普通编译预处理命令形式上略有差别。但它却十分之方便,稍微编译一下这个程序,则会在相应的目录下输出msado15.tlh和msado15.tli两个文件。 msado15.tlh包含了接口的声明,其内容看上去是下面这个样子的: // Created by Microsoft (R) C/C++ Compiler Version 12.00.8168.0 (a2f27f36). // // d:...debugmsado15.tlh // // C++ source equivalent of Win32 type library C:...adomsado15.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:....debugmsado15.tli // // Wrapper implementations for Win32 type library C:....adomsado15.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中自动接口查询带来的风险)。 但不管它如何,此时你知道了它的设计意图。这会帮助你理解这套智能指针的其他细节。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |