有效的使用和设计COM智能指针—条款4:理解ATL的CComPtr提倡简单
条款4:理解ATL的CComPtr提倡简单,高效
微软推出COM SDK后很快就意识到直接使用SDK开发COM是一件很困难的事情。于是他所做的第一件事情是将COM集成到MFC中去。但是随着Internet的发展,分布式组件要求COM能在网络上传输,但这却给MFC开发COM组件带来了相当大的障碍——MFC臃肿、庞大而且还要依赖很多DLL文件。在这种情况下ATL诞生了。
ATL【2】是ActiveX Template Library 的缩写。不同于MFC,ATL采用了如多继承和模版这两种C++的高阶编程技巧。开发人人员不仅能够快速地开发出高效、简洁的代码(Effective and Slim code),而且开发出的组件更加的轻便小巧。但由于模版和多继承的加入,学习ATL也更加复杂一些。
而CComPtr是ATL为了解决COM引用计数带来的问题,提供的一个类模版。因此你可能明白了。智能指针需要通过模版来实现(大多数智能指针考虑到通用性都是这么设计,也有少数例外)。因此你似乎明白了“智能指针”只是这种特定模版的一个较为通俗易懂的别名。它不会让人有种,因为使用了模版这类高级的C++编程技巧而产生高不可攀的感觉。同时在使用和它的行为上也更加类似于一个接口指针。
ATL除了提供CComPtr还提供了一个名为CComQiPtr的智能指针。两个智能指针的模版类用于实现对COM接口引用计数的自动管理,且都在<atlbase.h>中声明。
这两个模版类都继承自CComPtrBase,不同之处在于CComQiPtr能在必要的时候自动的对所需接口进行查询(如:对与此智能指针参数化类型不同的指针赋值时,会自动查询是否有所需的接口)。CComPtrBase类封装了CComPtr和CComQiPtr中公共的大多数函数,从而实现代码的复用。下图显示了这3个模版类的继承关系:
值得注意的是如果你的VC编译器版本过于老旧(比如vc6.0)则在ATL中无法使用到CComQiPtr,也看不到CComPtrBase这个基类。仅仅有CComPtr孤零零的呆在<atlbase.h>这个头文件中。这是因为在早期的ATL中,设计者只是设计了CComPtr这么一个模版类。而后期为了加入新的功能才将CComPtr和CComQiPtr的代码抽出来放入到CComPtrBase这个基类中去。
因此本文仅讨论CComPtr以及其基类CComPtrBase,而不涉及关于CComQiPtr的内容(有兴趣的读者可以查阅MSDN或阅读ATL源码了解其中的细节)。在CComPtr中除了构造函数和赋值运算符之外的大多数函数都源自于CComPtrBase,为了方便阅读和理解,本文会将这两个类中的成员函数放在一起讨论。
让我们再来看一次CComPtr的使用,或许进过一番介绍以后,你会对他内部如何管理引用计数有更加深刻的理解:
以IHello*为例,将程序中所有接口指针类型(除了参数),都使用CComPtr<IHello> 代替即可。即程序中除了参数之外,再也不要使用IHello*,全部以CComPtr<IHello>代替。
如下:
view plaincopy to clipboardprint?void SomeApp( IHello * pHello )
{
CComPtr<IHello> pCopy = pHello;
OtherApp();
pCopy->Hello();
}
void SomeApp( IHello * pHello )
{
CComPtr<IHello> pCopy = pHello;
OtherApp();
pCopy->Hello();
}
最后值得一提的是,虽然CComPtr的用法和普通COM接口指针类似,但是还是要主要如下几个问题:
1. CComPtr已经保证了AddRef和Release的正确调用,所以不需要,也不能够再调用AddRef和Release。
2. 如果要释放一个智能指针,直接给它赋NULL值即可。
3. CComPtr本身析构的时候会释放COM指针。
4. 当对CComPtr使用&运算符(取指针地址)的时候,要确保CComPtr为NUL。(因为通过CComPtr的地址对CComPtr赋值时,不会自动调用AddRef,若不为NULL,则前面的指针不能释放,CComPtr会使用assert报警)
ATL追求的是简洁与高效,因此在解决一个问题之时,CComPtr不会将解决问题的方式做到面面俱到。例如如果用一种方法能够解决这一问题,ATL尽可能不会采用第二种方法(除非这两种方法确实有不同之处)。
这一点从ATL的构造函数可以看出,他只有4个构造函数:1个必要的默认构造函数,1个必要的拷贝构造函数。还有2个为了满足指针语法而存在的转换构造函数。这样“恰当好处”就足够了。至于组件的创建过程,接口的查询过程,ATL认为这些东西都可以通过构造好之后的智能指针来完成。如下:
view plaincopy to clipboardprint?CComPtr<ICalculator> spCalculator= NULL;
hrRetCode = spCalculator.CoCreateInstance(CLSID_CALCULATOR);
KG_COM_ASSERT_EXIT(hrRetCode);
hrRetCode = spCalculator->Add(1, 2, &nSum);
KG_COM_ASSERT_EXIT(hrRetCode);
CComPtr<ICalculator> spCalculator= NULL;
hrRetCode = spCalculator.CoCreateInstance(CLSID_CALCULATOR);
KG_COM_ASSERT_EXIT(hrRetCode);
hrRetCode = spCalculator->Add(1, 2, &nSum);
KG_COM_ASSERT_EXIT(hrRetCode);
上述代码中你可能会怀疑智能指针没有满足RAII,但是他确实满足了。RAII并没有要求资源管理对象与资源同声明周期。他只是需要资源在获取之处就与一个资源管理对象绑定起来。ATL认为这样做最够了,而且代码清晰简单,那么他这样做了。
再来看看CComPtr的赋值运算符,他并没有做自动的接口查询工作(需要说明的是,CComPtr在后续版本中为部分赋值运算符加入了此种操作)。ATL认为有了QueryInte易做图ce这套接口,则开发人员就能方便的完成接口查询工作了。如下:
view plaincopy to clipboardprint?ICalculator* pCalculator = NULL;
CComPtr<IUnknown> spIUnknown = NULL;
hrRetCode = spIUnknown.CoCreateInstance(CLSID_CALCULATOR);
KG_COM_ASSERT_EXIT(hrRetCode);
hrRetCode = spIUnknown.QueryInte易做图ce(&pCalculator);//通过此函数查询接口
KG_COM_ASSERT_EXIT(hrRetCode);
hrRetCode = pCalculator->Add(1, 2, &nSum);
KG_COM_ASSERT_EXIT(hrRetCode);
ICalculator* pCalculator = NULL;
CComPtr<IUnknown> spIUnknown = NULL;
hrRetCode = spIUnknown.CoCreateInstance(CLSID_CALCULATOR);
KG_COM_ASSERT_EXIT(hrRetCode);
hrRetCode = spIUnknown.QueryInte易做图ce(&pCalculator);//通过此函数查询接口
KG_COM_ASSERT_EXIT(hrRetCode);
hrRetCode = pCalculator->Add(1, 2, &nSum);
KG_COM_ASSERT_EXIT(hrRetCode);
嗯~问题解决了,很符合ATL的哲学:简单!高效!如果想了解更多的关于赋值运算符重载中查询接口的问题可以查看条款26中“自动查询接口带来方便同时也潜藏危机”的论述。
作者“liuchang5的专栏”
补充:软件开发 , C语言 ,