参考书籍<<COM技术内幕>>
每个人都想使事情得以简化,我们看一下如何使COM组件的实现和使用更为容易。
1、 客户端的简化
COM组件不像典型的C++类的使用那样简单。首先一点是需要进行引用计数。若忘记了对某指针调用 AddRef,为了找出程序中的错误,可能需要将整个周末时间都赔进去。并且可能会访问指向某个已被释放的组件上接口的指针,从而导致应用程序的崩溃。即使我们在该调用Release的时候调用了它,但在程序中却不一定会真正调用它。
因为C++的异常处理程序不知道关于COM组件的任何信息。因此在发生了异常后,异常处理程序不会调用Release以释放此COM组件。正确地处理AddRef和Release只是万里长片中的第一步,下面还需要做放多工作以使调用QueryInterface 的代码合理化。
读者可能注意到,一个QueryInterface将会占用几行代码。一对QueryInterface调用极易使函数中实际的代码黯然失色。为避免此种问题,
我的作法通常是缓存接口指针,而不是在每次需要它的时候去查询这些接口。这种方法可以提高程序的效率及代码的可读性,付出的代价则是增大了内存开销。
但QuernInterface还有另一个更为严重的问题,即它不是类型安全的。如果某个不正确的指针传给了QueryInterface函数,编译器不会帮忙用户找出这一潜在的错误。
例如虽然下面代码将一个IY接口指针给了一个IZ接口指针,但编译器会通过:
IZ* pIA;
pIX->QueryInterface(IID_IY, (void**)&Piz);
会出现上述情况,关键在于那个可恶的void指针指向编译器隐藏了类型信息。对于上述这些问题,我们通过封装加以解决。为进行封装,有两种不同的方法,
其一是使用智能指针类来封装接口指针;另一种方法是通过包装类对接口本身进行封装。
1、1 智能接口指针
简化客户的第一种方法是通过智能指针而不是常规的接口指针来访问组件。智能接口指针的使用同常规的C++指针的使用是相同的。
它将把引用计数细节隐藏起来,并且当程序的执行离开了智能接口指针的作用域后相应的接口将被释放掉。这将使得对COM组件的使用同C++对象的使用是类似的。
一、什么是智能指针
一个智能指针实际上是一个重载了操作符->的类。智能接口指针类包含指向另外一个对象的指针。当用户调用智能指针上的->操作符时,智能指针把此调用转发给它所包含的指针所指的对象。智能接口中所包含的指针是指向一个接口的。
我们来看一个简单的例子,CFooPointer可以说得上是一个最简单的智能指针类,其中包含一个指针,并重载了->操作符。
class CFoo
{
public:
virtual void Bar();
};
class CFooPointer
{
public:
CFooPointer(CFoo *p)
{
m_p = p;
}
CFoo *operator->()
{
return m_p;
}
private:
CFoo *m_p;
};
void Funky(CFoo *pFoo)
{
// create and initialize the smart pointer
CFooPointer spFoo(pFoo);
// The following is the same as aFoo->Bar();
spFoo->Bar();
}
在上面的例子中,Funky函数创建了一个名为spFoo的CFooPointer对象并用pFoo对之进行初始化。spFoo指针将把相应的函数调用转发给m_p,
也就是pFoo。这样通过spFoo将可以调用到CFoo所实现的任何函数。这样做最妙的地方可能在于无需要明确地在CFooPointer中列出CFoo的
所有成员函数。对于CFoo来说,函数operator->表示的是“反引用我”;而对CFootPointer 来说则是“不要反引用我了,要就反引用m_p好了”。
CFootPointer作为一个智能指针是不够的,它实际上并没有做任何事情,一个具有良好优化能力的编译器可能会将CFootPointer优化得销声匿迹。同时它的行为
也不像一个指针,例如将pFoo值赋给spFoo是不可能的。这实际上将没法工作,因操作符=并没有被重载给m_p赋值。
为使客户相信一个CFooPointer同一个指向CFoo的指针是相同的,CFooPointer需要重载一系列其他的操作符,如*和&等。
因为这些操作符需要处理m_p指针而不是CFooPointer对象本身。
二、接口指针类的实现
用于处理COM接口的智能接口指针类没有字符串类那样丰富。在ActiveX模板库(ATL)中有两个名为CComPtr及CComQIPtr的COM接口指针类。
在Microsoft基本类库(MFC)中现在也有一个名为CIP类,只不过它是内部使用的。
CIF是一个全功能的智能指针接口指针,它几乎可能做任何事情。
下面给出作者的智能指针类实现,这里给出的类同ATL和MFC中给出的类是相似的,只不过功能没那么全。
我们给出的接口指针的名称为IPtr,程序清单给出了其实现。
//
// IPtr - Smart Interface Pointer
// Use: IPtr<IX, &IID_IX> spIX ;
// Do not use with IUnknown; IPtr<IUnknown, &IID_IUnknown>
// will not compile. Instead, use IUnknownPtr.
//
template <class T, const IID* piid> class IPtr
{
public:
// Constructors
IPtr()
{
m_pI = NULL ;
}
IPtr(T* lp)
{
m_pI = lp ;
if ( m_pI != NULL)
{
m_pI->AddRef() ;
}
}
IPtr(IUnknown* pI)
{
m_pI = NULL ;
if (pI != NULL)
{
pI->QueryInterface(*piid, (void **)&m_pI) ;
}
}
// Destructor
~IPtr()
{
Release() ;
}
// Reset
void Release()
{
if (m_pI != NULL)
{
T* pOld = m_pI ;
m_pI = NULL ;
pOld->Release() ;
}
}
// Conversion
operator T*() { return m_pI ;}
// Pointer operations
T& operator*() { assert(m_pI != NULL) ; return *m_pI ;}
T** operator&() { assert(m_pI == NULL) ; return &m_pI ;}
T* operator->() { assert(m_pI != NULL) ; return m_pI ;}
// Assignment from the same interface
T* operator=(T* pI)
{
if (m_pI != pI)
{
IUnknown* pOld = m_pI ; // Save current value.
m_pI = pI ; // Assign new value.
if (m_pI != NULL)
{
m_pI->AddRef() ;
}
if (pOld != NULL)
{
pOld->Release() ; // Release the old interface.
}
}
return m_pI ;
}
// Assignment from another interface
T* operator=(IUnknown* pI)
{
IUnknown* pOld = m_pI ; // Save current value.
m_pI == NULL ;
// Query for correct interface.
if (pI != NULL)
{
HRESULT hr = pI->QueryInterface(*piid, (void**)&m_pI) ;
assert(SUCCEEDED(hr) && (m_pI != NULL)) ;
}
if (pOld != NULL)
{
pOld->Release() ; // Release old pointer.
}
return m_pI ;
}
// Boolean functions
BOOL operator!() { return (m_pI == NULL) ? TRUE : FALSE ;}
// Requires a compiler that supports BOOL
operator BOOL() const
{
return (m_pI != NULL) ? TRUE : FALSE ;
}
// GUID
const IID& iid() { return *piid ;}
private:
// Pointer variable
T* m_pI ;
} ;
//
// IUnknownPtr is a smart interface for IUnknown.
//
class IUnknownPtr
{
public:
// Constructors
IUnknownPtr()
{
m_pI = NULL ;
}
IUnknownPtr(IUnknown* lp)
{
m_pI = lp ;
if ( m_pI != NULL)
{
m_pI->AddRef() ;
}
}
// Destructor
~IUnknownPtr()
{
Release() ;
}
// Reset
void Release()
{
if (m_pI)
{
IUnknown* pOld = m_pI ;
m_pI = NULL ;
pOld->Release() ;
}
}
// Conversion
operator IUnknown*() { return (IUnknown*)m_pI ;}
// Pointer operations
IUnknown& operator*() { assert(m_pI != NULL) ; return *m_pI ;}
IUnknown** operator&() { assert(m_pI == NULL) ; return &m_pI ;}
IUnknown* operator->() { assert(m_pI != NULL) ; return m_pI ;}
// Assignment
IUnknown* operator=(IUnknown* pI)
{
if (m_pI != pI)
{
IUnknown* pOld = m_pI ; // Save current value.
m_pI = pI ; // Assign new value.
if (m_pI != NULL)
{
m_pI->AddRef() ;
}
if (pOld != NULL) // Release the old interface.
{
pOld->Release() ;
}
}
return m_pI ;
}
// Boolean functions
BOOL operator!() { return (m_pI == NULL) ? TRUE : FALSE ;}
operator BOOL() const
{
return (m_pI != NULL) ? TRUE : FALSE ;
}
private:
// Pointer variable
IUnknown* m_pI ;
} ;
三、接口指针的使用
IPtr实现的使用是相当简单的,特别是对于模板类。产生可以通过一个接口类型及指向其IID的指针来创建一个接口指针对象。
然后可以调用CoCreateInstance来创建所需的组件并获取其指针了。
从下面的代码我们可以看到IPtr是如何有效地模拟真正的指针的。例如 我们可以对某个IPtr对象使用&操作符,如同它是一个真正指针那样。
void main()
{
IPtr<IX, &IID_IX> spIX;
HRESULT hr = ::CoCreateInstance(CLSID_COMPONENT1, NULL, CLSCTX_ALL, spIX->iid(), (void**)&spIX);
if (SUCCEEDED(hr))
{
spIX->Fx();
}
}
上面对于CoCreateInstance的调用并不是类型安全的,但可以通过在IPtr模板中定义另外一个函数以使之成为类型安全的。
HRESULT CreateInstance(const CLSID &clsid, IUnknown *pI, DWORD clsctx)
{
RELEASE();
return CoCreateInstance(clsid, pI, clsctx, *piid, (void **)&m_pI);
}
对于IPtr可按如下方式使用之:
IPtr<IX, &IID_IX> spIX;
HRESULT hr = spIX.CreateInstance(CLSID_COMPONENT1, NULL, CLSCTX_INPROC_SERVER);
另外可以看到智能接口指针变量的前面加上了一个前缀sp,以表示相应的指外为智能指针。
四、引用计数
在本例中,关于智能之针的最妙之处是我们无需记住去调用Release()。当程序的执行脱离开智能指针的作用域范围之外时,它将在其析构函数中自动调用Release。
这种作法还有另外一个好处是在产生了异常情况下,相应的接口也能够被释放掉,因此智能指针实际是一个C++对象。
若想将某个智能指针指向的接口释放掉,此时不能通过智能指针调用Relase。因智能指针不知道客户调用的函数是什么,而总是盲目地转发这些调用。
如果这样做的话,接口将被释放掉,但在智能指针的指针变量中却仍然保存着一个非空的接口指针值。此时如果接口再使用智能指针,将可能引起访问错误。
当希望释放接口时,不同的智能指针具有不同的方法。例如大多数智能指针(包括IPtr)将实现一个Release()函数。此Release函数将需要通过点记法来调用。
而不能使用->操作符:
spIX.Release();
释放IPtr 对应接口的另外一种方法是给它指定一个NULL值。
spIX = NULL;
到于语句为什么能够完成接口的释放,我们将在下面介绍。
注意:
CoInitialize(NULL);
CLSID clsid;
CLSIDFromProgID(OLESTR("myCom.GetRes"),&clsid);
CComPtr<IGetRes> pGetRes;//智能指针
pGetRes.CoCreateInstance(clsid);
pGetRes->Hello();
pGetRes.Release();//小心哦!!请看最后的“注意”
CoUninitialize();
COM中的智能指针实际上是重载了->的类,目的是为了简化引用记数,几乎不需要程序员显示的调用AddRef()和Release()。
但是为什么我们在上面代码中pGetRes.Release(),问题在于,我们的智能指针pGetRes生命周期的结束是在CoUninitialize()之后,CoInitialize所开的套间在CoUninitialize()后已经被关闭,而pGetRes此时发生析构,导致了程序的崩溃,
解决这个问题的另一个方法是加入{} 作用域,结束时就可析构:
CoInitialize(NULL);
CLSID clsid;
CLSIDFromProgID(OLESTR("myCom.GetRes"),&clsid);
{
CComPtr<IGetRes> pGetRes;//智能指针
pGetRes.CoCreateInstance(clsid);
pGetRes->Hello();
}
CoUninitialize();
五、赋值
IPtr类重载了=操作符,以便可以将一个接口指针赋值给IPtr包含的指针:
// Assignment from the same interface
T* operator=(T* pI)
{
if (m_pI != pI)
{
IUnknown* pOld = m_pI ; // Save current value.
m_pI = pI ; // Assign new value.
if (m_pI != NULL)
{
m_pI->AddRef() ;
}
if (pOld != NULL)
{
pOld->Release() ; // Release the old interface.
}
}
return m_pI ;
}
注意=操作符实现中两个令人感兴趣的方面。首先它将相应的调用AddRef和Release,这样在程序中就无需再进行这些调用了。
其次,智能接口指针将在给m_pl设置了新指针值之后释放当前的指针。这可以防止在赋值之前相应的组件被从内存中删除掉。
下面的代码将把pIX1指针赋给spIX的m_pI成员变量,赋值操作符将在此过程中调用相应接口上的AddRef。在使用完spIX 后,pIX2将被赋值给spIX。
此时=操作符将释放当前所包含接口指针(指向pIX1的指针),然后保存pIX2,并调用pIX2上的AddRef。
void Fuzzy(IX *pIX1, IX *pIX2)
{
IPtr<IX, IID_IX> spIX;
spIX = pIX1;
spIX->Fx();
spIX = pIX2;
spIX->Fx();
}
在IPtr的实现中提供了一个转换操作符,以便能够将一个IPtr 对象赋给相同类型的另外一个IPtr 对象。关于这点可参考以下例子:
typedef IPtr<IX, IID_IX> SPIX;
SPIX g_spIX;
void Wuzzy(SPIX spIX)
{
g_spIX = spIX;
}
此赋值操作符只在两个指针的类型相同时才能够工作。为提高代码的可读性我们使用了typedef语句。
六、未知接口赋值
我们的目标是要使用对QueryInterface调用得以简化。这可以通过对赋值操作符的另外一个重载来完成。
T *operator=(IUnknown *pIUnknown);
若将另外一个不同类型的接口指针赋值给一个智能指针,赋值操作符将自动调用QueryInterface。例如下面的代码中我们将一个IY接口指针赋给一个IX智能接口指针。
但应该注意的是,应对赋值后的智能指针进行检查,以决定此赋值是否成功。某此智能接口指针类在QueryInterface不成功将会引起一个异常。
void WasABear(IY *pIY)
{
IPtr<IX, IID_IX> spIX;
spIX = pIY;
if (spIX)
{
spIx->Fx();
}
}
就作者而言,并不赞成赋值操作符调用QueryInterface,因为我们知道关于重载有一条规则就是要保证重载后的操作符的行为同相应的内置操作符是相同。
这一点对于调用了QueryInterface的赋值操作符显然是不成立的。因为C++赋值不论如何都不会失败,但QueryInterface却可能会失败,所以调用了QueryInterface
的赋值操作符也可能会失败。
不幸的是,工业办却不遵循这一点。例如VB中就实现了一个调用QueryInterface的赋值操作符。
ATL和MFC中的智能指针也重载了赋值操作符并在其中调用了QueryInterface。
interface_case
作者更喜欢用一个名为interface_cast的函数来封装QueryInterface。此函数是一个模板函数,
程序员可以像使用dynamic_cast那样使用它,下面是interface_cast的实现。
template<calss I, const GUID *pGUID>
I *interface_cast(IUnknow *pIUnknown)
{
I *pI = NULL;
HRESULT hr = pIUnknown->QueryInterface(*pGUID, (void**)&pI);
assert(SUCCESSED(hr));
return PI;
}
为使用此函数可以按以下类似方法:
IY *pIY = interface_cast<IY, &IID_IY>(this);
函数interface_cast的优点在使用它时甚至不需要智能指针类,而其不利之处则在于它需要一个能够明确将模板函数实例化的编译器。
VS2005就是这样的一个编译器。
七、IUnknownPointer
P176