Download source files - 22 Kb
Download sample application - 113 Kb
The Problem
Many COM interfaces provide the ability to step through, or enumerate, a collection
of some kind. The usual way for a COM interface to expose this kind of functionality
is via an interface which conforms to the
IEnumXXXX
standard.
IEnum
interfaces provide the following methods: Next()
, Skip()
,
Reset()
and Clone()
. The most interesting is Next()
as this allows you to step along the collection that the interface provides access to.
Next()
is fairly complex though, as it allows you to fetch more than just
the next object, you can specify how many objects to fetch and get a whole block in one
go. This is to allow for situations where fetching the next object in the enumeration is
an expensive operation - such as if the interface referred to a remote object - and you
wished to reduce the number of calls to Next()
.
Because efficient iteration using an IEnumXXXX
interface requires a bit of
extra programmer effort to use I decided to write a template to wrap an
IEnumXXXX
interface in a class which provides an STL style iterator. As an
added bonus, you can then write STL style template code which can use the IEnum
iterator or any other read only forward iterator.
The use of this class converts code like this:
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]);
}
}
}
into code like this:
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>
IEnumIterator
is a template class which wraps an
IEnum
interface pointer and provides "easier" access to the underlying sequence. The
template is designed to be derived from and the derived class passes itself as
the first parameter to the template, this is required so that the template can
provide correct functionality for its post increment operator (which needs to
save a copy of the iterator prior to incrementing it) and for the static
end()
function which provides access to an iterator that represents
the end of any sequence. The second parameter to the template is the
IEnum
interface that we're providing a wrapper for. The final parameter is the object
that the
IEnum
interface iterates over. By providing this information
the
IEnumIterator
can automatically provide a conversion operator
from itself to the underlying object that we're iterating over. This allows you
to use the iterator as if it were an instance of the underlying object, thus
simplifying code which iterates over the sequence.
The iterator provides the following functionality:
- The constructor allows the creator of the iterator to specify the number
of items to cache inside the iterator each time the underlying
IEnum
interface has its Next()
member called -
this can be changed by the client by a call to setCacheSize()
.
setCacheSize()
- allows the client to change the number of
items returned each time the underlying IEnum
interface has its
Next()
member called.
- Pre-increment:
++it
- move the iterator to the next item in the
sequence.
- Post-increment:
it++
- move the iterator to the next item in the
sequence and return a copy of the iterator prior to it being incremented. Note
this has a significant overhead when compared to pre-increment. A copy of the
current state of the iterator must be made prior to incrementing the iterator.
This requires a duplication of the sequence cache and a call to
Clone()
on the underlying iterator pointer. Post-increment should
be avoided unless theses semantics are truly required. To prevent post-increment
being used by mistake, the functionality is only included if
IENUM_ITERATOR_USE_POST_INC
is defined prior to inclusion of the
IEnumIterator.hpp file.
operator "E"()
- cast the iterator to the current item
in the sequence. Throws a NullIterator exception if the iterator is no longer valid.
Skip()
allows you to move the iterator forward by a specified amount.
Reset()
allows you to position the iterator at the beginning of the
sequence again.
Derived classes
The
IEnumIterator
template requires you to derive from it before you can
use it. The simplest derived class is shown below:
class CIterateGUID
: public IEnumIterator<CIterateGUID, IEnumGUID, GUID>
{
public :
CIterateGUID(IEnumGUID *pIEnumGUID)
: IEnumIterator<CIterateGUID, IEnumGUID, GUID>(pIEnumGUID)
{
}
};
Although often the style of derivation show above is all that you'll need, you can get
more adventurous if you like:
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; }
};
The above example provides us with a wrapper to an IEnumCATEGORYINFO
interface and allows us to use the iterator to access all of the elements of the
underlying CATEGORYINFO
object that we're iterating. The call to
Enumerated()
gives us access to the underlying object and from there we
can perform any operations we might like on it. The wrappers shown above aren't
strictly necessary, we can always just convert the iterator into an object of
the required type and access it directly, but sometimes they might make things
neater.
Ownership of items being iterated over
My original attempt at this class didn't allow you to use
Skip()
and
Reset()
. I decided to add this functionality to make the wrapper
more complete. Of course, this added more complications, and it also pointed out
some glaring omissions from the first cut of the design... The possibility of
skipping items in the sequence means that some items may be fetched from the
underlying enumeration interface and cached inside the wrapper but then never
accessed. This is fine unless the caller is responsible for managing the
lifetime the objects that are returned, as would be the case with an
IEnumUknown
interface for example. To implement
Skip()
and
Reset()
the iterator had to be responsible for the lifetime of the
objects it was iterating. This was achieved by having a virtual function in the
iterator called to destroy a item each time the iterator was advanced and another
virtual function called whenever there was a need to copy an item. The derived
class for an
CIterateIUnknown
iterator would look something like the
one shown below:
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; }
};
This means that the caller is no longer responsible for lifetime of the pointers
returned. If the caller wishes to continue to use a pointer after stepping the
iterator along, they must call AddRef()
on it to take ownership,
and then Release()
it as normal when they're through.
Efficiency issues
In my first attempt design of the iterator, the first call to the underlying interface
pointer's
Next()
member function was made during construction of the
IEnumIterator
. This meant that if code returned an
IEnumIterator
to a client who wished to change the number of items cached, the cache has already have
been loaded before the client could call
setCacheSize()
. It also resulted
in unnecessary copying of cached items.
The current version of the code keeps track of whether or not the iterator has been
"primed" or not. This makes the code slightly more complex, but only from an
implementation point of view. The interface is unchanged for the client. The new code
calls Next()
for the first time when either the iterator is incremented
(if the cache is not primed we need to prime it and then step along one item...) or
when the underlying object is accessed (if the cache is not primed we prime it and
then return the first item). This means that it's possible for the client to change
the cache size before the first call of Next()
and that when passing an
iterator around by value there's no need to copy a cache of items unless the iterator
has been used before it is copied.
See the article on Len's homepage for the latest updates.