COM接口迭代器封装技术

在COM编程中,经常需要遍历某种集合。通常,COM接口通过遵循IEnumXXXX标准的方式来暴露这种功能。IEnum接口提供了Next()、Skip()、Reset()和Clone()等方法。其中,Next()方法最为关键,因为它允许遍历接口提供的集合。Next()方法相对复杂,因为它允许一次获取多个对象,而不仅仅是下一个对象。这样做的目的是为了减少在枚举过程中对Next()方法的调用次数,尤其是在接口引用远程对象时,每次调用Next()都可能是一个昂贵的操作。

为了提高使用IEnumXXXX接口的效率,需要程序员额外努力。因此,决定编写一个模板类,将IEnumXXXX接口封装在一个类中,提供类似STL风格的迭代器。这样,就可以编写类似STL风格的模板代码,使用IEnum迭代器或其他只读前向迭代器。

使用这个类,可以将以下代码:

void DoThingWithGUID(GUID &guid) { // ... } void EnumerateGUIDs(IEnumGUID *pIEnum) { GUID guids[64]; ULONG numFetched = 0; while (SUCCEEDED(pIEnum->Next(64, guids, &numFetched))) { for (ULONG i = 0; i < numFetched; i++) { DoThingWithGuid(guids[i]); } } }

转换为以下代码:

void DoThingWithGUID(GUID &guid) { // ... } void EnumerateGUIDs(IEnumGUID *pIEnum) { CIEnumGUID it(pIEnum, 64); while (it != CIEnumGUID::end()) { DoThingWithGuid(it); ++it; } }

IEnumIterator<class T, class I, class E>是一个模板类,它封装了一个IEnum接口指针,并提供了对底层序列的“更容易”访问。模板设计为派生使用,派生类将自身作为模板的第一个参数传递,这是必需的,以便模板可以为其后缀递增运算符(需要在递增迭代器之前保存迭代器的副本)和静态end()函数提供正确的功能,该函数提供了一个表示任何序列末尾的迭代器。模板的第二个参数是正在提供包装的IEnum接口。最后一个参数是IEnum接口迭代的对象。通过提供这些信息,IEnumIterator可以自动提供一个从自身到正在迭代的底层对象的转换运算符。这允许使用迭代器,就像它是底层对象的一个实例一样,从而简化了迭代序列的代码。

迭代器提供了以下功能:

  • 构造函数允许迭代器的创建者指定每次调用底层IEnum接口的Next()成员时缓存的项目数量 - 这可以通过客户端调用setCacheSize()来更改。
  • setCacheSize() - 允许客户端更改每次调用底层IEnum接口的Next()成员时返回的项目数量。
  • 前缀递增:++it - 将迭代器移动到序列中的下一个项目。
  • 后缀递增:it++ - 将迭代器移动到序列中的下一个项目,并返回迭代器在递增之前的副本。与前缀递增相比,这有显著的开销。必须在递增迭代器之前制作当前迭代器状态的副本。这需要复制序列缓存并对底层迭代器指针调用Clone()。后缀递增应该避免使用,除非这些语义确实需要。为了防止意外使用后缀递增,只有在定义IENUM_ITERATOR_USE_POST_INC之前包含IEnumIterator.hpp文件时,才包含该功能。
  • 运算符"E"() - 将迭代器转换为序列中的当前项目。如果迭代器不再有效,则抛出NullIterator异常。
  • Skip()允许通过指定的数量将迭代器向前移动。
  • Reset()允许将迭代器重新定位到序列的开始。

IEnumIterator模板需要派生它才能使用它。下面展示了最简单的派生类:

class CIterateGUID : public IEnumIterator<CIterateGUID, IEnumGUID, GUID> { public: CIterateGUID(IEnumGUID *pIEnumGUID) : IEnumIterator<CIterateGUID, IEnumGUID, GUID>(pIEnumGUID) { } };

虽然上面展示的派生风格通常是需要的全部,但如果愿意,可以更冒险一些:

class CIterateCATEGORYINFO : public IEnumIterator<CIterateCATEGORYINFO, IEnumCATEGORYINFO, CATEGORYINFO> { public: CIterateCATEGORYINFO(IEnumCATEGORYINFO *pIEnumCATEGORYINFO) : IEnumIterator<CIterateCATEGORYINFO, IEnumCATEGORYINFO, CATEGORYINFO>(pIEnumCATEGORYINFO) { } CATID GetCATID() const { return Enumerated().catid; } LCID GetLCID() const { return Enumerated().lcid; } LPCOLESTR GetDescription() const { return Enumerated().szDescription; } };

上述示例为提供了一个IEnumCATEGORYINFO接口的包装器,并允许使用迭代器访问正在迭代的底层CATEGORYINFO对象的所有元素。调用Enumerated()使能够访问底层对象,从那里可以执行可能需要的任何操作。上面展示的包装器并不是绝对必要的,总是可以直接将迭代器转换为所需的对象类型并直接访问它,但有时它们可能会让事情更整洁。

正在迭代的项目所有权:

最初尝试的这个类不允许使用Skip()和Reset()。决定添加这个功能,使包装器更完整。当然,这增加了更多的复杂性,并且也指出了第一版设计中的一些明显遗漏...在序列中跳过项目的可能性意味着一些项目可能从未被访问过,尽管它们已经从底层枚举接口中获取并缓存在包装器中。这很好,除非调用者负责管理返回的对象的生命周期,就像IEnumUknown接口一样。为了实现Skip()和Reset(),迭代器必须负责它正在迭代的对象的生命周期。这是通过在迭代器中有一个虚拟函数来实现的,每次迭代器前进时都会调用该函数来销毁一个项目,另一个虚拟函数在需要复制项目时调用。CIterateIUnknown迭代器的派生类看起来像下面这样:

class CIterateIUnknown : public IEnumIterator<CIterateIUnknown, IEnumIUnknown, IUnknown *> { public: CIterateIUnknown(IEnumIUnknown *pIEnumIUnknown) : IEnumIterator<CIterateIUnknown, IEnumIUnknown, IUnknown *>(pIEnumIUnknown) { } virtual void Destroy(IUnknown *pItem) const { pItem->Release(); } virtual IUnknown *Copy(IUnknown *pItem) const { pItem->AddRef(); return pItem; } };

这意味着调用者不再负责返回的指针的生命周期。如果调用者希望在迭代器前进后继续使用指针,他们必须调用AddRef()来拥有它,然后在他们完成时像往常一样调用Release()。

效率问题:

在最初的迭代器设计中,第一次调用底层接口指针的Next()成员函数是在IEnumIterator的构造期间进行的。这意味着如果代码将IEnumIterator返回给客户端,而客户端希望更改缓存的项目数量,缓存在客户端可以调用setCacheSize()之前就已经加载了。这也导致了不必要的缓存项目复制。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485