COM: IEnumXXXX 到 STL 风格迭代器包装器类





5.00/5 (1投票)
枚举对象集合的简化方法。
问题
许多 COM 接口都提供了步进或枚举某种集合的能力。COM 接口公开此类功能的常用方法是通过符合IEnumXXXX
标准的接口。
IEnum
接口提供以下方法:Next()
、Skip()
、Reset()
和 Clone()
。其中最有趣的是 Next()
,因为它允许您沿着接口提供的集合进行步进。但是,Next()
相当复杂,因为它允许您获取的不仅仅是下一个对象,您可以指定要获取的对象数量,一次获取一个大块。这是为了适应以下情况:枚举下一个对象可能是一项昂贵的操作——例如,如果接口指向远程对象——而您希望减少对 Next()
的调用次数。
由于使用 IEnumXXXX
接口进行高效迭代需要程序员付出一些额外的努力,因此我决定编写一个模板来将 IEnumXXXX
接口包装在一个提供 STL 风格迭代器的类中。作为额外的奖励,您还可以编写 STL 风格的模板代码,可以使用 IEnum
迭代器或任何其他只读前向迭代器。
使用此类会将类似这样的代码
void DoThingWithGUID(GUID &guid) { } void EnumerateGUIDs(IEnumGUID *pIEnum) { GUID guids[64]; // Cache up to 64 items each time we call Next() 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); // Cache up to 64 items each time we call Next() while (it != CIEnumGUID::end()) { DoThingWithGuid(it); ++it; } }
IEnumIterator<class T, class I, class E>
IEnumIterator
是一个模板类,它包装一个 IEnum
接口指针并提供对底层序列的“更轻松”访问。该模板设计为可派生,派生类将其自身作为第一个参数传递给模板,这是必需的,以便模板可以为其后置递增运算符(需要保存迭代器副本才能递增)和静态 end()
函数(提供对代表任何序列末尾的迭代器的访问)提供正确的功能。传递给模板的第二个参数是我们正在为其提供包装器的 IEnum
接口。最后一个参数是 IEnum
接口所迭代的对象。通过提供此信息,IEnumIterator
可以自动提供从自身到我们正在迭代的底层对象的转换运算符。这允许您像使用底层对象实例一样使用迭代器,从而简化了遍历序列的代码。迭代器提供以下功能
- 构造函数允许迭代器的创建者指定每次调用底层
IEnum
接口的Next()
成员函数时在迭代器内部缓存的项目数——客户端可以通过调用setCacheSize()
来更改此值。 setCacheSize()
- 允许客户端更改每次调用底层IEnum
接口的Next()
成员函数时返回的项目数。- 前置递增:
++it
- 将迭代器移动到序列中的下一个项目。 - 后置递增:
it++
- 将迭代器移动到序列中的下一个项目,并在迭代器递增之前返回其副本。请注意,与前置递增相比,后置递增具有显着的开销。在递增迭代器之前必须制作当前迭代器状态的副本。这需要复制序列缓存并调用底层迭代器指针的Clone()
。除非真正需要这些语义,否则应避免使用后置递增。为了防止意外使用后置递增,此功能仅在包含 IEnumIterator.hpp 文件之前定义了IENUM_ITERATOR_USE_POST_INC
时才包含。 operator "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()
之前,缓存就已经加载了。这也导致了缓存项目的不必要复制。当前版本的代码会跟踪迭代器是否已被“预加载”。这使得代码稍微复杂一些,但仅从实现的角度来看。客户端的接口没有改变。新代码在迭代器被递增时(如果缓存未预加载,我们需要预加载然后前进一个项目……)或在访问底层对象时(如果缓存未预加载,我们预加载然后返回第一个项目)第一次调用 Next()
。这意味着客户端可以在第一次调用 Next()
之前更改缓存大小,并且在按值传递迭代器时,除非在复制迭代器之前使用过它,否则无需复制项目缓存。
有关最新更新,请参阅 Len 主页上的 文章。