有效的使用和设计COM智能指针 ——条款5:了解_com_ptr_t 设计背后的历史原因

来源:互联网 发布:十三水算法 编辑:程序博客网 时间:2024/05/19 02:02

条款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 MyClassset objVar = new MyOtherClassobjVar.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 MyClassMyOtherClassPtr spMyOtherClass(CLSID_MYOTHERCLASS); spMyClass = spMyOtherClass;     //set objVar = new MyOtherClassspMyClass.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中自动接口查询带来的风险)。

但不管它如何,此时你知道了它的设计意图。这会帮助你理解这套智能指针的其他细节。