previous page
next page

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.

Figure 8.2. The CComEnum inheritance hierarchy


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.


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)