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.>



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: August 20, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT When you look at natural user interfaces as a developer, it isn't just fun and games. There are some very serious, real-world usage models of how things can help make the world a better place – things like Intel® RealSense™ technology. Check out this upcoming eSeminar and join the panel of experts, both from inside and outside of Intel, as they discuss how natural user interfaces will likely be getting adopted in a wide variety …

  • Live Event Date: August 14, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Data protection has long been considered "overhead" by many organizations in the past, many chalking it up to an insurance policy or an extended warranty you may never use. The realities of today makes data protection a must-have, as we live in a data-driven society -- the digital assets we create, share, and collaborate with others on must be managed and protected for many purposes. Check out this upcoming eSeminar and join Seagate Cloud …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds