有效的使用和设计COM智能指针 ——条款5:了解_com_ptr_t 设计背后的历史原因
来源:互联网 发布:十三水算法 编辑:程序博客网 时间:2024/05/19 02:02
条款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, 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的支持。而VB、delphi、javascript则更是在语法层面上支持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编译器还会允许它通过编译?
在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 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中自动接口查询带来的风险)。
但不管它如何,此时你知道了它的设计意图。这会帮助你理解这套智能指针的其他细节。
- 有效的使用和设计COM智能指针 ——条款5:了解_com_ptr_t 设计背后的历史原因
- 有效的使用和设计COM智能指针——条款6:尽量以智能指针替换接口指针
- 有效的使用和设计COM智能指针——条款14:有意识的限制智能指针的生命周期
- 有效的使用和设计COM智能指针——条款1:智能指针之前世今生
- 有效的使用和设计COM智能指针——条款21:巧妙的将对象伪装成指针
- 有效的使用和设计COM智能指针——条款2:引用计数的是与非
- 有效的使用和设计COM智能指针——条款4:理解ATL的CComPtr提倡简单,高效
- 有效的使用和设计COM智能指针——条款15:以原则中的优先级作为取舍的依据
- 有效的使用和设计COM智能指针—条款4:理解ATL的CComPtr提倡简单
- 有效的使用和设计COM智能指针——条款18:重载运算符不应当扭曲其语义
- 有效的使用和设计COM智能指针——条款23:为例外条件准备应对策略。
- 有效的使用和设计COM智能指针 ——条款16:智能指针的引入不能违反COM引用计数规则
- 有效的使用和设计COM智能指针——条款10:尽量减少智能指针和接口指针的混用
- 有效的使用和设计COM智能指针 ——条款13:必须提前释放COM组件时,别妄想智能指针帮你完成
- 有效的使用和设计COM智能指针——条款3:按照功能和实现原理选择合适的智能指针
- 有效的使用和设计COM智能指针——条款22:果断放弃二进制重用,而采用模版编写智能指针
- 有效的使用和设计COM智能指针——条款12:必要时使用attach() 和 detach()调整引用计数
- 有效的使用和设计COM智能指针——条款11:以类型安全的方式创建资源和查询接口
- Android系统移植-按键移植
- CopyU! v2怎么了?
- 全球主要语言缩写对照(微软测试)
- This message is looping: it already has my Delivered-To line
- ORA-01591错误的原因和处理方法
- 有效的使用和设计COM智能指针 ——条款5:了解_com_ptr_t 设计背后的历史原因
- 常用几种数据库连接字符串
- Windows 下 C语言的头文件后缀H和h是没有区别的
- 微软的一道经典逻辑推理题:小明和小强都是张老师的学生,张老师的生日是M月N日
- 求教msgget(key, 0644 | IPC_CREAT)是什么意思,0644代表什么?谢谢~~~
- jQuery Validation插件remote验证方式的Bug
- 浮躁的世界里 我们要的是生活
- JavaScript 中设置或读取Cookie
- 高通Android display架构分析