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

条款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,__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的支持。而VBdelphijavascript则更是在语法层面上支持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编译器还会允许它通过编译?

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 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中自动接口查询带来的风险)。

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Format[$] ( expr [ , fmt ] ) format 返回变体型 format$ 强制返回为文本 -------------------------------- 数字类型的格式化 --------------------------------     固定格式参数:     General Number 普通数字,如可以用来去掉千位分隔号     format$("100,1
VB6或者ASP 格式化时间为 MM/dd/yyyy 格式,竟然没有好的办法, Format 或者FormatDateTime 竟然结果和系统设置的区域语言的日期和时间格式相关。意思是尽管你用诸如 Format(Now, "MM/dd/yyyy"),如果系统的设置格式区域语言的日期和时间格式分隔符是"-",那他还会显示为 MM-dd-yyyy     只有拼凑: <%response.write
在项目中添加如下代码:新建窗口来显示异常信息。 Namespace My ‘全局错误处理,新的解决方案直接添加本ApplicationEvents.vb 到工程即可 ‘添加后还需要一个From用来显示错误。如果到这步还不会则需要先打好基础啦 ‘======================================================== ‘以下事件
转了这一篇文章,原来一直想用C#做k3的插件开发,vb没有C#用的爽呀,这篇文章写与2011年,看来我以前没有认真去找这个方法呀。 https://blog.csdn.net/chzjxgd/article/details/6176325 金蝶K3 BOS的插件官方是用VB6编写的,如果  能用.Net下的语言工具开发BOS插件是一件很愉快的事情,其中缘由不言而喻,而本文则是个人首创,实现在了用V
Sub 分列() ‘以空格为分隔符,连续空格只算1个。对所选中的单元格进行处理 Dim m As Range, tmpStr As String, s As String Dim x As Integer, y As Integer, subStr As String If MsgBox("确定要分列处理吗?请确定分列的数据会覆盖它后面的单元格!", _
  窗体代码 1 Private Sub Text1_OLEDragDrop(Data As DataObject, Effect As Long, Button As Integer, Shift As Integer, X As Single, Y As Single) 2 Dim path As String, hash As String 3 For Each fil
  Imports MySql.Data.MySqlClient Public Class Form1 ‘ GLOBAL DECLARATIONS Dim conString As String = "Server=localhost;Database=net2;Uid=root;Pwd=123456;" Dim con As New MySqlConnection
‘導入命名空間 Imports ADODB Imports Microsoft.Office.Interop   Private Sub A1() Dim Sql As String Dim Cnn As New ADODB.Connection Dim Rs As New ADODB.Recordset Dim S As String   S = "Provider=OraOLEDB.Oracl
Imports System.IO Imports System.Threading Imports System.Diagnostics Public Class Form1 Dim A(254) As String    Function ping(ByVal IP As Integer) As String Dim IPAddress As String IPAddress = "10.0.
VB运行EXE程序,并等待其运行结束 参考:https://blog.csdn.net/useway/article/details/5494084 Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long Pr
今天碰到一个问题,登陆的时候,如果不需要验证手机号为空,则不去验证手机号 因为登陆的时候所有的验证信息都存放在一个数组里 Dim CheckUserInfo() As String ={UserBirthday, SecEmail, UserMob, UserSex, RealNameFirst, RealName, CheckCardID, CheckCardType, Contactemail
在VB6.0中,数据访问接口有三种: 1、ActiveX数据对象(ADO) 2、远程数据对象(RDO) 3、数据访问对象(DAO) 1.使用ADO(ActiveX Data Objec,ActiveX数据对象)连接SQL Server 1)使用ADO控件连接 使用ADO控件的ConnectionString属性就可以连接SQL Server,该属性包含一个由分号分隔的argument=value语
注:大家如果没有VB6.0的安装文件,可自行百度一下下载,一般文件大小在200M左右的均为完整版的软件,可以使用。   特别提示:安装此软件的时候最好退出360杀毒软件(包括360安全卫士,电脑管家等,如果电脑上有这些软件的话),因为现如今的360杀毒软件直接会对VB6.0软件误报,这样的话就可能会在安装过程中被误报阻止而导致安装失败,或者是安装后缺乏很多必须的组件(其它的杀毒软件或安全卫士之类的
Private Sub Form_Load() Call conndb End Sub Private Function conndb() Dim cn As New ADODB.Connection Dim rs As New ADODB.Recordset Dim strCn, sql As String Dim db_host As String Dim db_user As String
  PPSM06S70:  Add  moddate  EDITSPRINTJOB:  MAX(TO_CHAR(ETRN.MODDATE, ‘yyyy/mm/dd/HH24:MI AM‘)) ACTUAL_SHIPDATE   4.Test Scenario (1) :Query SQL Test DN:8016578337 SELECT CTRN.TKCTID TRUCK_ID,        
  沒有出現CrystalReportViewer時,須安裝CRforVS_13_0. 新增1個數據集,新增1個數據表,添加二列,列名要和資料庫名一樣. 修改目標Framework 修改app.config, <startup >改成<startup useLegacyV2RuntimeActivationPolicy ="true">  CrystalReport1.rpt增加數據庫專家 在表單
Imports System.Threading Imports System Public Class Form1 Dim th1, th2 As Thread Public Sub Method1() Dim i As Integer For i = 1 To 100 If Me.Label1.BackColor =
Friend Const PROCESS_ALL_ACCESS = &H1F0FFF = 2035711 Friend Const PROCESS_VM_READ = &H10 Friend Const PROCESS_VM_WRITE = &H20 Friend Const PAGE_READONLY = &H2 Friend Const PAGE_READWRITE = &H4 Friend
以下代码随手写的 并没有大量测试 效率也有待提升 如果需要C#的请自行转换 Function SplitBytes(Data As Byte(), Delimiter As Byte()) As List(Of Byte()) Dim i = 0 Dim List As New List(Of Byte()) Dim bytes As New
Imports System.Data.SqlClient Public Class Form1 REM Public conn1 As SqlConnection = New SqlConnection("server=.; Integrated Security=False;Initial Catalog= mydatabase1; User ID= sa;password")