Simple STL Collections in ATL

This article requires VC++ 6.0.

Lately I’ve had to create a variety of ATL collections and have used many different methods.
ATL 3.0 provides many different templates to help do this, but may favorite ATL collection
helper, IEnumOnSTLImpl isn’t covered in the docs at all. As its name suggests,
it provides implementation for the IEnumXXXX interfaces using STL container classes.

To demonstrate how this template helps us create collection classes, I’ll create a simple
collection object which (mostly) mimics the Collection object provided in Visual Basic 6.0.

The .IDL for this object looks like this:


[
object,
uuid(4B738073-EA47-11D2-B25A-00105A022091),
dual,
helpstring(“IVBCollection Interface”),
pointer_default(unique)
]
interface IVBCollection : IDispatch
{
[id(DISPID_VALUE)]
HRESULT Item( [in] VARIANT* Index, [out, retval] VARIANT* pvarRet);

[id(0x00000001)]
HRESULT Add( [in] VARIANT* Item, [in, optional] VARIANT* Key);

[propget, id(0x00000002)]
HRESULT Count([out, retval] long* pi4);

[id(0x00000003)]
HRESULT Remove([in] VARIANT* Index);

[propget, id(DISPID_NEWENUM)]
HRESULT _NewEnum([out, retval] IUnknown** ppunk);
};

With the exception of some changes to the Add() method, this .IDL is the same as the one
you can extract from the VB6 typelib.

We will use a few typedefs to help make these template classes easier to work with.
The VB documentation describes a string Key which can be used to identify an element, so we
will use an STL map class for our internal storage. Using a CComBSTR with an STL container
requires you to use the CAdapt template helper. See ATLBASE.H for more information
about CAdapt. Here is the internal storage typedef for this project:


typedef std::map<CAdapt<CComBSTR>, CComVariant> VarMap;

Using an STL map with IEnumOnSTLImpl requires us to provide a custom copy policy. A copy
policy is a tricky way of customizing the behavior of a template class (like IEnumOnSTLImpl)
without changing its code. Copy policies have a standard form, with static copy(), init() and
destroy() methods. These methods are used by IEnumOnSTLImpl to move things into and out of
internal storage.


class _CopyVarMapToVariant
{
public:
static HRESULT copy(VARIANT* pCopy, std::pair<const CAdapt<CComBSTR>, CComVariant>* pIt)
{
CComVariant v = pIt->second;
v.Detach(pCopy);
return S_OK;
}
static void init(VARIANT* p) {VariantInit(p);}
static void destroy(VARIANT* p) {VariantClear(p);}
};

One of the template parameters for IEnumOnSTLImpl is the collection interface we intend to
support. For this project we will use IEnumVARIANT because using it allows us to support VB’s
For..each syntax.

The following typedef shows how to tie all of this stuff together so we can derive a class
from it.


typedef IEnumOnSTLImpl<IEnumVARIANT, &IID_IEnumVARIANT, VARIANT, _CopyVarMapToVariant, VarMap> VarEnumImpl;

Now we can put our ATL class together. For this example, our object will be creatable so we will
derive it from CComCoClass as well as CComObjectRootEx. We will inherit IDispatch support from
IDispatchImpl, and finally IEnumVARIANT support from our own typedef, VarEnumImpl.


class ATL_NO_VTABLE CVBCollection :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CVBCollection, &CLSID_VBCollection>,
public IDispatchImpl<IVBCollection, &IID_IVBCollection, &LIBID_STDCOLLECTIONLib>,
public VarEnumImpl
{

}

The beauty of this method is how little code you have to write. Typically, implementations
of the _NewEnum() method involve quite a bit of code for copying data from one array to another.
But since our class will now derive from IEnumXXXX, we can just return the this pointer
cast to IUnknown*.


STDMETHODIMP CVBCollection::get__NewEnum(IUnknown** pVal)
{
// Make sure the internal STL iterator is at the beginning
// by calling IEnumVARIANT::Reset()
Reset();

// Cast back to IUnknown*
*pVal = static_cast<IUnknown*>(static_cast<IEnumVARIANT*>(this));
(*pVal)->AddRef();

return S_OK;
}

Implementing the get_Count() method is even simpler:


STDMETHODIMP CVBCollection::get_Count(long *pVal)
{
*pVal = m_coll.size();
return S_OK;
}

The remaining methods are pretty simple as well — notice that we reuse our copy policy
in the Item() method. If we weren’t trying to follow VB’s semantics, we wouldn’t have to do
all the VARIANT type checking and these methods would simply become pass-thrus to the STL
container.


STDMETHODIMP CVBCollection::Item(VARIANT* Index, VARIANT* pVal)
{
// According to the VB documentation Index must
// either be numeric or a string
if( V_VT(Index) == VT_BSTR )
{
CComBSTR bstrKey( V_BSTR(Index) );
VarMap::iterator it = m_coll.find(bstrKey);
_CopyVarMapToVariant::copy(pVal, &(*it));
}
else if( (V_VT(Index) == VT_I2))
{
short nIndex = V_I2(Index);
VarMap::iterator it = m_coll.begin();
VarMap::iterator end = m_coll.end();

for( int i=0; i<nIndex && it != end; i++, it++ ) ;

if( it == end ) // We got to the end without finding it
return E_FAIL;
else
_CopyVarMapToVariant::copy(pVal, &(*it));
}
else if( (V_VT(Index) == VT_I4))
{
long nIndex = V_I2(Index);
VarMap::iterator it = m_coll.begin();
VarMap::iterator end = m_coll.end();

for( int i=0; i<nIndex && it != end; i++, it++ ) ;

if( it == end ) // We got to the end without finding it
return E_FAIL;
else
_CopyVarMapToVariant::copy(pVal, &(*it));
}
else
return E_INVALIDARG;

return S_OK;
}

STDMETHODIMP CVBCollection::Add( /* [in] */ VARIANT* Item, /* [in, optional] */ VARIANT* Key)
{
CComVariant varAdded(*Item);
CComBSTR bstrKey;

// Determine if the optional param Key is present
if((V_VT(Key) != VT_ERROR) && (V_ERROR(Key) != DISP_E_PARAMNOTFOUND))
{
// According to the VB documentation, key must be a string
if( V_VT(Key) == VT_BSTR )
bstrKey = V_BSTR(Key);
else
return E_INVALIDARG;
}
else
{
// We don’t have a key, so make one from the next unique counter
TCHAR szKey[128];
wsprintf(szKey, “Key_%li”, nUniqueKeyCounter++);
bstrKey = szKey;
}

m_coll[bstrKey] = varAdded;

return S_OK;
}

STDMETHODIMP CVBCollection::Remove( /* [in] */ VARIANT* Index)
{
// According to the VB documentation Index must
// either be numeric or a string. I’ll assume they mean
// only VT_I4 or VT_I2

if( V_VT(Index) == VT_BSTR )
{
CComBSTR bstrKey( V_BSTR(Index) );
m_coll.erase(bstrKey);
}
else if( (V_VT(Index) == VT_I2))
{
short nIndex = V_I2(Index);
VarMap::iterator it = m_coll.begin();
VarMap::iterator end = m_coll.end();

for( int i=0; i<nIndex && it != end; i++, it++ ) ;

if( it == end ) // We got to the end without finding it
return E_FAIL;
else
m_coll.erase(it);
}
else if( (V_VT(Index) == VT_I4))
{
long nIndex = V_I2(Index);
VarMap::iterator it = m_coll.begin();
VarMap::iterator end = m_coll.end();

for( int i=0; i<nIndex && it != end; i++, it++ ) ;

if( it == end ) // We got to the end without finding it
return E_FAIL;
else
m_coll.erase(it);
}
else
return E_INVALIDARG;

return S_OK;
}

I have found this method to be a very quick way to implement VBScript compatible collections
using STL storage containers. I have used it to store IDispatch-derived interface pointers (using
CComPtr and CAdapt) while implementing simple object models and for many other
simple collections.

This method works well with all of the STL containers, including vector and list, although
you’ll have to change the copy policy. It should work with multimap as well, although I haven’t
tried it.

Download source – 17 KB

This VC++ 6.0 project file contains a VBScript file you can use to test the object. You
must have a VBScript interpreter (like the Windows Scripting Host or the Visual Studio
Macro interpreter) installed to use this script.>

More by Author

Must Read