65.9K
CodeProject 正在变化。 阅读更多。
Home

防止循环引用的 ATL 对象集合

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.90/5 (9投票s)

2003年9月23日

CPOL

11分钟阅读

viewsIcon

70283

downloadIcon

1804

使用 ATL 和 STL 创建 COM 对象集合, 而不会产生循环引用。

引言

COM 对象集合的一个常见问题是,当集合的每个成员都需要能够回溯引用到父集合对象时。网络上有许多文章试图使用弱引用来解决这个问题,但如果弱引用的对象被客户端销毁,这些方法存在严重的可靠性问题。当需要维护集合的集合时,这个问题会变得更加严重。

本文演示了一种可能的解决方案,该方案实现起来非常容易。我在演示文件中包含了小的测试程序,它们除了确认所有对象引用计数在正确的时间归零外,并没有做其他事情。它包含一个 ATL COM exe 项目(在 VC++ 2003 中)来实现模板,以及一个小的 VB6.0 项目来演示按任意顺序创建和删除对象。通过查看 Windows 任务管理器,观察可执行文件自行卸载,可以很容易地看到,当所有对象都被销毁时,引用计数都回到了零。

使用代码

我设计了 2 个文件,它们构成了源代码下载,可以包含在任何需要此类集合的 ATL 项目中。CollectionDefs.idl 可以在项目 IDL 中 #include,在导入 oaidl.idlocidl.idl 之后。然后,在任何需要这些接口实现的模块头文件中,包含 CollectionHelpers.h 文件。

从这里开始,我将使用 Department/Employee 示例来演示这些头文件的用法。要使用这些模板,首先定义你的集合的 Coclass 和 Interface。此时,你也应该至少声明你的集合成员的 Coclass 和 Interface。然后我们开始实现集合对象。

在 Helpers 模块中实现的标准属性和方法包括 AddRemove 方法,以及 get_Itemget__NewEnumget_Count 属性。你可以为你的集合接口公开你自己的接口,然后简单地调用辅助类来实现这些函数。还包括了一个 IEnumVARIANT 的实现,因此 _NewEnum 属性返回一个 VB 兼容的集合。

// CollectionTest.idl
// ...
interface IDepartment : IDispatch{
  [propget, id(DISPID_VALUE), helpstring("property Item")] 
    HRESULT Item([in] VARIANT Index, [out, retval] IEmployee** pVal);
  [id(DISPID_NEWENUM), propget] HRESULT _NewEnum(
    [out, retval]IUnknown **ppUnk);
  [propget, id(1), helpstring("property Name")] 
    HRESULT Name([out, retval] BSTR* pVal);
  [propput, id(1), helpstring("property Name")] 
    HRESULT Name([in] BSTR newVal);
  [id(2), helpstring("method Add")] HRESULT Add([in] 
    IEmployee* pEmployee);
  [id(3), helpstring("method Remove")] HRESULT Remove([in] VARIANT Index);
  [propget, id(4), helpstring("property Count")] 
    HRESULT Count([out, retval] long* pVal);
};

一旦在 C++ 头文件中定义了 Coclass,只需从 CContainer 继承它即可。CContainer 类是模板化的,将你的集合成员暴露的接口和应使用的 STL 容器类型作为模板参数。第一个模板参数几乎是不言自明的(在我们的例子中是 IEmployee 接口),但第二个参数需要一些考虑,并且必须遵守一些先决条件。我们的例子使用了一个 map 类,以便能够根据名称快速查找员工。

class ATL_NO_VTABLE CDepartment : 
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CDepartment, &CLSID_Department>,
  public IDispatchImpl<IDepartment, &IID_IDepartment,
   &LIBID_CollectionTestLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
  public CContainer<IEmployee, std::map<CComBSTR, IMemberData*> >
{
  ...
};

我已经为头文件中的容器编写了实现代码的 4 种 STL 容器类型:list、vector、set 和 map。每种容器都保存着一个 IMemberData 接口实现的引用,最终由你来编写。map 容器仅实现为接受 CComBSTR 类型索引。

实际集合存储在 CContainer 对象的成员变量 m_coll 中,其类型为 CSTLContainer<YourSTLContainerType>。此类直接继承自 YourSTLContainerType,但重载了 STL 集合中许多常见的 find、append、insert 和 erase 方法。因此,无论使用哪种集合类型,都可以在你自己的方法中以一致的方式操作 m_coll。它还有一个 Reference() 方法,该方法接受一个 STL 迭代器作为参数,并返回指向你的 IMemberObject 的指针。

为了在你的集合中实现标准的集合类型方法和属性,只需将它们引用到 CContainer 对象中的实现即可。

STDMETHODIMP CDepartment::Remove(VARIANT Index) {
  return Container::Remove(Index);
}

STDMETHODIMP CDepartment::get_Count(long* pVal) {
  return Container::get_Count(pVal);
}

STDMETHODIMP CDepartment::get__NewEnum(IUnknown **ppUnk) { 
   return Container::get__NewEnum(ppUnk); 
 }
 
STDMETHODIMP CDepartment::Add(IEmployee* pEmployee) {
  CComQIPtr<IMember> pMember = pEmployee;
  if (!pMember)
    return E_INVALIDARG;
  return Container::Add(pEmployee);
}

STDMETHODIMP CDepartment::get_Item(VARIANT Index, IEmployee** pVal) {
  CComPtr<IMember> pMember;
  HRESULT hr = Container::get_Item(Index, &pMember);
  if (SUCCEEDED(hr))
    hr = pMember.QueryInterface(pVal);
  return hr;
}

请注意,Removeget_Countget_NewEnum 直接调用 CContainer 的实现(Container 只是 CContainer<typename IExposed, typename STLContainer> 的内部 typedef)。Add 方法顶部有几行代码,只是为了确保传递的对象确实实现了 IMember(这是一个先决条件)。否则,它也只是调用 CContainer 方法。CContainerget_Item 方法实际上返回 IMember 接口,然后该接口被查询以获取 IEmployee 接口返回给客户端。

下一步是实现你的成员对象。这需要更多的工作,因为这是处理循环引用情况的地方。它的工作方式是,你的成员对象的实现(减去任何父引用)在一个完全不同的 Coclass 中实现。你的实际暴露对象仅引用此 CMemberData 类,以设置和检索其状态属性。

实现此目的的方法是定义成员对象的接口,然后复制并重命名类定义。这意味着需要修改一些 ATL 生成的代码,但工作量不大。当然,你需要重命名构造函数,以及 BEGIN_COM_MAP 条目。由于此类不向客户端公开,因此应删除其对 ComCoClass 的继承,并将 DECLARE_REGISTRY 条目更改为 DECLARE_NO_REGISTRY。之后,让你的新数据类继承自 CMemberData,并让你的暴露的 coclass 继承自 CMember。再次强调,这些类是模板化的,以允许辅助类相互交叉引用。

CMemberData 类接受一个模板参数,即你暴露的 coclass 的实现。这允许 CMemberData 对象创建你的 coclass 的实例,插入对自身的引用,并将此新创建的对象返回给客户端。这是避免循环引用的方法。重要的是 CMemberData 对象**不包含对父集合的任何引用**。所有向上的引用都由你暴露对象中的 CMember 实现持有。

// CEmployeeData
class ATL_NO_VTABLE CEmployeeData : 
  public CComObjectRootEx<CComSingleThreadModel>,
  //public CComCoClass<CEmployee, &CLSID_Employe>,
  public IDispatchImpl<IEmployee, &IID_IEmployee, 
    &LIBID_CollectionTestLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
  public CMemberData<CEmployee>
{
public:
  CEmployeeData() { }

DECLARE_NO_REGISTRY ( )

BEGIN_COM_MAP(CEmployeeData)
  COM_INTERFACE_ENTRY(IEmployee)
  COM_INTERFACE_ENTRY(IDispatch)
  COM_INTERFACE_ENTRY(IMemberData)
END_COM_MAP()
//...
protected:  // Property Data
  CComBSTR  m_sName;
public:
  STDMETHOD(get_Name)(BSTR* pVal) { *pVal = m_sName.Copy(); return S_OK; }
  STDMETHOD(put_Name)(BSTR newVal) { m_sName = newVal; return S_OK; }
  STDMETHOD(get_Department)(IDepartment** pVal) { return E_NOTIMPL; }
};

请注意,此数据类也实现了你的暴露接口。这并非完全必要,但可以简化实现。实际上,它不需要实现任何接口,只需要实现 IMemberData,而 IMemberData 实际上已经被你继承的 CMemberData 类实现了。它需要能够为你的暴露成员类提供最小的状态信息。但是,通过实现暴露接口,你的暴露对象只需通过其 m_pData 成员将所有状态信息请求直接传递给数据类。引用父集合的属性(在这种情况下是 get_Department)仅返回 E_NOTIMPL

此外,还有许多与 IMemberData 类相关的辅助方法,用于提高 STL 集合的效率或数据检索。这些方法允许你的 set 或 map 集合基于对象本身或从对象获取的字符串键执行比较。涉及的方法有五种:两种用于对象比较,三种用于键比较。通过实现其中一个或两个方法组,STL 能够根据你的对象内容快速查找它们。

用于对象比较的方法原型是

HRESULT IMemberData::CanCompareObjects(void);
HRESULT IMemberData::CompareObject(IMemberData* pObject, short *pResult);

CanCompareObjects 仅在实现 CompareObject 方法时返回 S_OK,否则返回 S_FALSECompareObject 实现后,其工作方式与库字符串比较函数相同,根据正在比较的对象是否小于、等于或大于正在比较的对象,分别返回负数、零或正数。实现这些方法可以高效地存储和引用 IMemberObjects 集合。

或者,可以使用 map 集合来索引对象,索引基于 BSTR 键。通过实现键比较方法,可以有效地将具有嵌入式键的对象添加到集合中,然后根据该键值访问这些对象。键访问方法的原型是

HRESULT IMemberData::CanCompareKeys();
HRESULT IMemberData::CompareKey(BSTR Key, short *pResult);
HRESULT IMemberData::get_Key(BSTR *pKey);

这些工作方式与 CompareObject 方法基本相同,只是增加了 get_Key() 方法来从完整对象中提取键。

最后,要完成集合的实现,你需要从 CMember 派生你的暴露的 coclass。此类接受 2 个模板参数,即你的 IMemberData 实现的 Coclass,以及此 coclass 暴露的接口。如果此接口与你的 CMemberData 派生类中实现的接口相同,则在此类中实现接口就像调用数据类中的相同方法一样简单。

HRESULT CExposedClass::MyMethod(Param1, Param2) { 
  return CDataClass::MyMethod(Param1, Param2);
}

这种设置的唯一区别在于你想引用父集合时。请记住,数据类不能有对父集合的引用,否则整个系统将失效,并导致循环引用。因此,CMember 实现持有一个对集合的完整(非弱)引用,称为 m_pParent,它可以返回给客户端应用程序。

HRESULT CExposedClass::get_Parent(IMyCollection **ppVal) {
  return m_pParent->QueryInterface(ppVal);
}

一旦所有这些都实现完毕,你就会拥有一个引用安全的集合,集合中的每个成员都持有指向父集合的安全链接。只要客户端持有对集合中任何成员、集合对象本身或集合枚举器的引用,就可以保证所有对象都将保持完整。一旦客户端释放了所有这些引用,对象就会被正确销毁,资源就会被释放回你的应用程序。

工作原理

除了实现 STL 容器上的索引的那些棘手的代码细节之外,这个集合背后的思想非常简单。CContainer 类有一个成员,即 STL 容器本身。当它被填充时,它是由 CMemberData 派生对象填充的。这些对象包含正确实现你要收集的对象所需的状态信息。然而,这些对象不引用容器本身,因此对于客户端持有的每个引用,容器的引用计数都保持为 1。MemberData 对象的引用计数也为 1,因为它是被容器对象引用的。在容器被销毁之前,它们不会被销毁。

如果客户端创建一个新的 Member 对象,它还会在内部创建一个 MemberData 对象来保存其状态信息。这个 MemberData 对象被 CMemberm_pData 成员引用。当这个对象被添加到集合中时,CContainer 的 add 方法实际上将 m_pData 对象放入集合中,然后将 CMember 类的 m_pParent 成员设置为指向自身。此时,Container 和 MemberData 对象的引用计数增加到 2,而 CMember 的引用计数保持为 1。释放客户端中的 CMember 对象将导致对象被销毁,而其状态信息在集合中保持不变。此时,ContainerMemberData 对象的引用计数将减回到 1。

当客户端从集合中检索成员时,实现实际上会创建一个新的 CMember 实例,然后用集合中的 CMemberData 对象替换此新对象的 C++ 状态数据。它还会设置新对象中的 m_pParent 引用。

如果客户端释放了对 Container 对象的持有,同时保留对 Member 对象的引用,则由于 Member 中的 m_pParent 引用,Container 将保持不变。因此,集合中的所有 MemberData 对象都将保持不变,因为它们被 Container 引用。只有当 Container 和 Container 的任何 Member 的所有客户端引用都被释放后,Container 才会被销毁。实际的 Member 对象仅由客户端引用,因此客户端对其生命周期拥有完全控制权。

实现摘要

总之,请遵循以下 10 个简单步骤来实现一个防循环引用的集合。

  1. 使用 ATL 向导定义你的集合类和接口。
  2. 使用 ATL 向导定义你的集合成员的类和接口。
  3. 让你的集合类继承自 CContainer<IExposed, STLType>,并在 COM_MAP 中添加对 IContainer 接口的引用。
  4. 复制并重命名生成头文件中的成员类定义,记住删除复制的类对 CComCoClass 的继承,将其改为 DECLARE_NO_REGISTRY,并在构造函数和 COM_MAP 声明中更改类名。
  5. 让你的复制的类继承自 CMemberData<CMemberCoClass>。在类的 COM_MAP 中添加对 IMember 接口的引用。
  6. 让你的原始成员类继承自 CMember<CMemberDataClass, IExposed>,并在类的 COM_MAP 中添加对 IMember 的引用。
  7. 在你的原始对象的副本(继承自 CMemberData 的那个)中编写你的成员实现代码。
  8. 通过 CMemberm_pData 成员来引用 MemberData 的属性和方法。
  9. 通过返回对 CMemberm_pParent 成员生成的引用来实现 (get_)Parent 属性或方法。
  10. 通过引用 CContainer 类中预定义的 C++ 方法来 C++ 实现你的集合对象。
© . All rights reserved.