在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可以自动提供一个从自身到正在迭代的底层对象的转换运算符。这允许使用迭代器,就像它是底层对象的一个实例一样,从而简化了迭代序列的代码。
迭代器提供了以下功能:
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()之前就已经加载了。这也导致了不必要的缓存项目复制。