Enumerating
Arrays
CComEnum
Because enumeration interfaces are all the same
except for the actual data being enumerated, their implementation
can be standardized, given a couple assumptions. Depending on how
you've stored your data, you can use one of two ATL enumeration
interface classes. The most flexible implementation class enables
you to provide your data in a standard C++-like collection. This is
called CComEnum-OnSTL (discussed later). The simplest
implementation assumes that you've stored your data as an array.
It's called CComEnum, and the complete implementation is
as follows:
template <class Base, const IID* piid, class T, class Copy,
class ThreadModel = CComObjectThreadModel>
class ATL_NO_VTABLE CComEnum :
public CComEnumImpl<Base, piid, T, Copy>,
public CComObjectRootEx< ThreadModel > {
public:
typedef CComEnum<Base, piid, T, Copy > _CComEnum;
typedef CComEnumImpl<Base, piid, T, Copy > _CComEnumBase;
BEGIN_COM_MAP(_CComEnum)
COM_INTERFACE_ENTRY_IID(*piid, _CComEnumBase)
END_COM_MAP()
};
Although this implementation consists of only a
few lines of code, there's quite a lot going on here. The template
arguments are as follows:
-
Base is the enumeration interface to be
implementedfor example, IEnumPrimes.
-
piid is a pointer to the interface
being implementedfor example, &IID_IEnumPrimes.
-
piid T is the type of data being
enumeratedfor example, long.
-
Copy is the class responsible for
copying the data into the client's buffer as part of the
implementation of Next. It can also be used to cache a
private copy of the data in the enumerator to guard against
simultaneous access and manipulation.
-
ThreadModel describes just how thread
safe this enumerator needs to be. When you specify nothing, it uses
the dominant threading model for objects, as described in Chapter 4, "Objects in ATL." Of
course, because a COM enumerator is a COM object like any other, it
requires an implementation of IUnknown. Toward that end,
CComEnum derives from CComObjectRootEx. You'll
see later that I further derive CComObject from
CComEnum to fill in the vtbl
properly.
Really, CComEnum is present simply to
bring CComObjectRootEx together with
CComEnumImpl, the base class that actually implements
Next, Skip, Reset, and Clone.
Figure 8.2 shows how these
classes fit together.
Copy Policy
Classes
The fundamental job of the
enumerator is to copy the collection's data into the buffer that
the client provides. If the data being enumerated is a pointer or a
structure that contains pointers, a simple memcpy or
assignment will not do the trick. Instead, the client needs its own
deep copy of each element, which it can release when it has
finished with it. Toward that end, ATL enumerators use a class
called a copy policy class, often
just called a copy policy, to
scope static methods for dealing with deep-copy semantics. The
static methods of a copy policy are like the Increment and
Decrement methods of the threading model classes, except
that instead of incrementing and decrementing a long, copy
policies know how to initialize, copy, and destroy data. For simple
types, ATL provides a template copy policy class:
template <class T>
class _Copy {
public:
static HRESULT copy(T* p1, const T* p2) {
Checked::memcpy_s(p1, sizeof(T), p2, sizeof(T));
return S_OK;
}
static void init(T*) {}
static void destroy(T*) {}
};
Given an array of a simple type (such as
long), this template works just fine:
HRESULT CopyRange(long* dest, long* src, size_t count) {
for (size_t i = 0; i != count; ++i) {
HRESULT hr = _Copy<long>::copy(&dest[i], &src[i]);
if( FAILED(hr) ) {
while( i > 0 ) _Copy<long>::destroy(&dest[i]);
return hr;
}
}
return S_OK;
}
However, given something with trickier
semantics, such as a VARIANT or an OLESTR,
memcpy is too shallow. For the four most commonly
enumerated data types, ATL provides specializations of the
_Copy template:
template<> class _Copy<VARIANT>;
template<> class _Copy<LPOLESTR>;
template<> class _Copy<OLEVERB>
template<> class _Copy<CONNECTDATA>;
For example, the copy policy for
VARIANTs looks like this:
template<> class _Copy<VARIANT> {
public:
static HRESULT copy(VARIANT* p1, const VARIANT* p2) {
p1->vt = VT_EMPTY;
return VariantCopy(p1, const_cast<VARIANT*>(p2));
}
static void init(VARIANT* p) {p->vt = VT_EMPTY;}
static void destroy(VARIANT* p) {VariantClear(p);}
};
If you're dealing with interface pointers,
again, the _Copy template won't do, but building your own
specialization for each interface you want to copy is a bit
arduous. For interfaces,
ATL provides the _CopyInterface copy policy class
parameterized on the type of interface you're managing:
template <class T> class _CopyInterface {
public:
static HRESULT copy(T** p1, T** p2) {
ATLENSURE(p1 != NULL && p2 != NULL);
*p1 = *p2;
if (*p1)
(*p1)->AddRef();
return S_OK;
}
static void init(T** ) {}
static void destroy(T** p) {if (*p) (*p)->Release();}
};
Using copy policies, we now have a generic way
to initialize, copy, and delete any kind of data, making it easy to
build a generic and safe duplication routine:
template <typename T, typename Copy>
HRESULT CopyRange(T* dest, T* src, size_t count) {
for (size_t i = 0; i != count; ++i) {
HRESULT hr = Copy::copy(&dest[i], &src[i]);
if( FAILED(hr) ) {
while( i > 0 ) Copy::destroy(&dest[i]);
return hr;
}
}
return S_OK;
}
CComEnumImpl's implementation of the
Next method uses the copy policy passed as the template
parameter to initialize the client's buffer and fill it with data
from the collection, much like our sample CopyRange
routine. However, before we jump right into the Next
method, let's see how CComEnumImpl does its job.
CComEnumImpl
To implement the methods of an enumeration
interface, CComEnumImpl maintains five data members:
template <class Base, const IID* piid, class T, class Copy>
class ATL_NO_VTABLE CComEnumImpl : public Base {
public:
CComEnumImpl();
virtual ~CComEnumImpl();
STDMETHOD(Next)(ULONG celt, T* rgelt, ULONG* pceltFetched);
STDMETHOD(Skip)(ULONG celt);
STDMETHOD(Reset)(void)
STDMETHOD(Clone)(Base** ppEnum);
HRESULT Init(T* begin, T* end, IUnknown* pUnk,
CComEnumFlags flags = AtlFlagNoCopy);
CComPtr<IUnknown> m_spUnk;
T* m_begin;
T* m_end;
T* m_iter;
DWORD m_dwFlags;
...
};
The
m_begin, m_end, and m_iter members are
each pointers to the type of data being enumerated, as passed via
the T template parameter. Each of these members keeps
track of pointers into an array of the data being enumerated. In
classic standard C++ style, m_begin points to the
beginning of the array, m_end points to one past the end
of the array, and m_iter points to the next element to
hand out. The m_dwFlags member determines if and when to
copy initialization data that the creator of the enumerator
provides. The m_spUnk member refers to the owner of the
data if the enumerator is sharing it instead of keeping its own
copy. The implementations of Next, Skip,
Reset, and Clone use these variables to provide
their behavior. These variables are set in the Init method
of CComEnumImpl.
Initializing
CComEnumImpl
Calling the Init method requires the
data to be arranged in an array. Maybe the collection is already
maintaining the data as an array, or maybe it's not. Either way,
the begin parameter to Init must be a pointer to
the beginning of an array of the type being enumerated, and the
end parameter must be one past the end of the same array.
Where that array comes from and how the enumerator manages it
depend on the last parameter to Init, the flags
parameter. This parameter can take one of three values:
-
AtlFlagNoCopy means that the collection
already maintains its data in an array of the type being enumerated
and is willing to share the data with the enumerator. This is more
efficient because the enumerator doesn't keep its own copy;
it
merely initializes m_begin, m_end, and
m_iter to point at the collection's data. However, this
can lead to unpredictable results if a client uses the collection
to modify the data while it's being enumerated.
If you use the AtlFlagNoCopy flag, you
should pass an interface pointer to the collection that owns the
data as the pUnk parameter to Init. The
enumerator caches this interface pointer, adding to the reference
count of the collection. This is necessary to keep an enumerator
from outliving the collection and, more important, the data that
the collection is maintaining. For each of the other two flags,
pUnk is NULL.
-
AtlFlagCopy means that the collection
already maintains the data in the appropriate format but would
prefer the enumerator to have its own copy of the data. This is
less efficient but ensures that no manipulation of the collection
affects the data that the enumerator maintains.
-
AtlFlagTakeOwnership means that the
collection doesn't maintain its data in an array of a type
appropriate for the enumerator to use. Instead, the collection has
allocated an array of the data type being enumerated using
operator new[] for sole use of the enumerator. When the
enumerator is destroyed, it should destroy its copy of the data
using operator delete[]. This is especially handy for the
implementation of IEnumVARIANT because most developers
prefer to keep data in types more specific than VARIANT
but are willing to provide an array of VARIANTs when
creating the enumerator.
CComEnumImpl
Implementation
The most interesting part of the
CComEnumImpl implementation is the Next method.
Recall that Next's job is to copy the client-requested
number of elements into the client-provided buffer.
CComEnumImpl's implementation of the Next method
is identical in concept to the CopyRange function I showed
you earlier. Next uses the copy policy to copy the data
provided by the collection at initialization into the client's
buffer. If anything goes wrong, the copy policy is used to destroy
the data already copied. The rest of the logic is argument
validation and involves watching for the end of the data.
template <class Base, const IID* piid, class T, class Copy>
STDMETHODIMP CComEnumImpl<Base, piid, T, Copy>::Next(
ULONG celt, T* rgelt,
ULONG* pceltFetched) {
if (pceltFetched != NULL)
*pceltFetched = 0;
if (celt == 0)
return E_INVALIDARG;
if (rgelt == NULL || (celt != 1 && pceltFetched == NULL))
return E_POINTER;
if (m_begin == NULL || m_end == NULL || m_iter == NULL)
return E_FAIL;
ULONG nRem = (ULONG)(m_end - m_iter);
HRESULT hRes = S_OK;
if (nRem < celt)
hRes = S_FALSE;
ULONG nMin = celt < nRem ? celt : nRem ;
if (pceltFetched != NULL)
*pceltFetched = nMin;
T* pelt = rgelt;
while(nMin) {
HRESULT hr = Copy::copy(pelt, m_iter);
if (FAILED(hr)) {
while (rgelt < pelt)
Copy::destroy(rgelt++);
if (pceltFetched != NULL)
*pceltFetched = 0;
return hr;
}
pelt++;
m_iter++;
}
return hRes;
}
The implementations of Skip and
Reset are trivial:
template <class Base, const IID* piid, class T, class Copy>
STDMETHODIMP CComEnumImpl<Base, piid, T, Copy>::Skip(ULONG celt) {
if (celt == 0)
return E_INVALIDARG;
ULONG nRem = ULONG(m_end - m_iter);
ULONG nSkip = (celt > nRem) ? nRem : celt;
m_iter += nSkip;
return (celt == nSkip) ? S_OK : S_FALSE;
}
template <class Base, const IID* piid, class T, class Copy>
STDMETHODIMP CComEnumImpl<Base, piid, T, Copy>::Reset()
{ m_iter = m_begin;return S_OK; }
The
Clone method is responsible for duplicating the current
enumerator. This means creating a new enumerator of the same type
and initializing it using the Init method. However, the
data is never copied again for subsequent enumerators. Instead, if
the collection indicated that the data was to be shared, a new
enumerator gets the IUnknown* of the original collection,
giving the collection another reason to live. Otherwise, if the
enumerator is keeping its own copy of the data, the new enumerator
is given the IUnknown* of the original enumerator. Because
enumerators are read-only, one copy of the data serves for all
enumerators.
template <class Base, const IID* piid, class T, class Copy>
STDMETHODIMP CComEnumImpl<Base, piid, T, Copy>::Clone (
Base** ppEnum) {
typedef CComObject<CComEnum<Base, piid, T, Copy> > _class;
HRESULT hRes = E_POINTER;
if (ppEnum != NULL) {
*ppEnum = NULL;
_class* p;
hRes = _class::CreateInstance(&p);
if (SUCCEEDED(hRes)) {
// If this object has ownership of the data then we
// need to keep it around
hRes = p->Init(m_begin, m_end, (m_dwFlags & BitOwn) ?
this : m_spUnk);
if (SUCCEEDED(hRes)) {
p->m_iter = m_iter;
hRes = p->_InternalQueryInterface(*piid, (void**)ppEnum);
}
if (FAILED(hRes))
delete p;
}
}
return hRes;
}
CComEnum Use
As an example of a typical CComEnum
use, let's implement the IPrimeNumbers collection
interface:
[dual]
interface IPrimeNumbers : IDispatch {
HRESULT CalcPrimes([in] long min, [in] long max);
[propget]
HRESULT Count([out, retval] long* pnCount);
[propget, id(DISPID_VALUE)]
HRESULT Item([in] long n, [out, retval] long* pnPrime);
[propget, id(DISPID_NEWENUM)]
HRESULT _NewEnum([out, retval] IUnknown** ppunkEnum);
};
The collection maintains
a list of the prime numbers in a C++ vector. The
Calc-Primes method populates the collection:
STDMETHODIMP CPrimeNumbers::CalcPrimes(long min, long max) {
m_rgPrimes.clear();
for (long n = min; n <= max; ++n ) {
if (IsPrime(n)) m_rgPrimes.push_back(n);
}
return S_OK;
}
The get_Count and get_Item
methods use the vector to perform their duties:
STDMETHODIMP CPrimeNumbers::get_Count(long* pnCount) {
*pnCount = m_rgPrimes.size();
return S_OK;
}
STDMETHODIMP CPrimeNumbers::get_Item(long n, long* pnPrime) {
// Oh, let's be 1-based today...
if (n < 1 || n > m_rgPrimes.size()) return E_INVALIDARG;
*pnPrime = m_rgPrimes[n-1];
return S_OK;
}
Because we're going out of our way to support VB
with our collection interface, the get__NewEnum method
returns an interface on an implementation of IEnumVARIANT.
Because the name of the parameterized enumerator is used more than
once, it's often handy to use a type definition:
typedef CComEnum< IEnumVARIANT, &IID_IEnumVARIANT, VARIANT,
_Copy<VARIANT> > CComEnumVariant;
Remember, the
CComEnum template parameters are, in order, the interface
we'd like the enumerator to implement, the IID of that interface,
the type of data we'd like to enumerate, and, finally, a copy
policy class for copying the data from the enumerator's copy to the
client's buffer. To provide an implementation of IUnknown,
the CComEnum class is further used as the base class for a
new CComObject class. Using this type definition, the
implementation of get__NewEnum entails creating an
instance of an enumerator, initializing it with array data, and
filling ppunkEnum with a pointer to the enumerator for use
by the client. Because we're keeping the data as a vector, however,
we have to allocate an array of VARIANTs manually, fill
the data from the vector, and pass ownership to the enumeration
using AtlFlagTakeOwnership. The following code illustrates
this procedure:
STDMETHODIMP CPrimeNumbers::get__NewEnum(IUnknown** ppunkEnum) {
*ppunkEnum = 0;
// Create an instance of the enumerator
CComObject<CComEnumVariant>* pe = 0;
HRESULT hr = CComObject<CComEnumVariant>::CreateInstance(&pe);
if (SUCCEEDED(hr)) {
pe->AddRef();
// Copy data from vector<long> to VARIANT*
size_t nPrimes = m_rgPrimes.size();
VARIANT* rgvar = new VARIANT[nPrimes];
if (rgvar) {
ZeroMemory(rgvar, sizeof(VARIANT) * nPrimes);
VARIANT* pvar = &rgvar[0];
for (vector<long>::iterator it = m_rgPrimes.begin();
it != m_rgPrimes.end();
++pvar, ++it ) {
pvar->vt = VT_I4;
pvar->lVal = *it;
}
// Initialize enumerator
hr = pe->Init(&rgvar[0], &rgvar[nPrimes], 0,
AtlFlagTakeOwnership);
if (SUCCEEDED(hr)) {
// Fill outbound parameter
hr = pe->QueryInterface(IID_IUnknown, (void**)ppunkEnum);
}
}
else {
hr = E_OUTOFMEMORY;
}
pe->Release();
}
return hr;
}
Unfortunately, this code
leaves an unpleasant taste in one's mouth. Although it would have
been considerably simpler if we'd already had an array of
VARIANTs holding the data, frankly, that's rare. C++
programmers tend to use containers other than the error-prone C++
array. Because of this tendency, we were forced to translate the
data from our preferred format to the preferred format of the ATL
enumerator implementation. Given the regularity of a container's
C++ interface, this seems like a waste. In an ideal world, we'd
have an enumeration implementation that could handle a standard C++
container instead of an array. In an ideal world, we'd have
CComEnumOnSTL. Welcome to my ideal world.
|