Collections: How to Make a Type-Safe Collection
This article explains how to make type-safe collections for your own data types. Topics include:
Using template-based classes for type safety
Implementing helper functions
Using nontemplate collection classes
The Microsoft Foundation Class Library provides predefined type-safe collections based on C++ templates. Because they are templates, these classes provide type safety and ease of use without the type-casting and other extra work involved in using a nontemplate class for this purpose. The MFC Advanced Concepts sample demonstrates the use of template-based collection classes in an MFC application. In general, use these classes any time you write new collections code.
Using Template-Based Classes for Type Safety
To use template-based classes
Declare a variable of the collection class type. For example:
CList<int, int> m_intList;
Call the member functions of the collection object. For example:
m_intList.AddTail( 100 ); m_intList.RemoveAll( );
If necessary, implement the , , and . For information on implementing these functions, see Implementing Helper Functions.
This example shows the declaration of a list of integers. The first parameter in step 1 is the type of data stored as elements of the list. The second parameter specifies how the data is to be passed to and returned from member functions of the collection class, such as Add and GetAt.
Implementing Helper Functions
The template-based collection classes CArray, CList, and CMap use seven global helper functions that you can customize as needed for your derived collection class. For information on these helper functions, see in the Class Library Reference. Three of these helper functions are used in constructing, destructing, and serializing collection elements; implementations of these functions are necessary for most uses of the template-based collection classes.
Construction and Destruction
The helper functions and are called by members that respectively add and remove elements from a collection.
Helper | Called directly by | Called indirectly by |
ConstructElements | ||
DestructElements |
You should override these functions if their default action is not suitable for your collection class. The default implementation of ConstructElements sets to 0 the memory that is allocated for new elements of the collection and calls the constructor of each element. The default implementation of DestructElements calls the destructor for each element.
In general, overriding ConstructElements is necessary whenever the collection stores objects that require a call to a constructor (or other initializing function), or when the objects have members requiring such calls. Overriding DestructElements is necessary when an object requires special action, such as freeing memory allocated from the heap, when the object is destroyed.
For example, you might override ConstructElements for an array of CPerson objects as follows:
class CPerson : public CObject { . . . };
CArray< CPerson, CPerson& > personArray;
template <> void AFXAPI ConstructElements <CPerson> ( CPerson* pNewPersons, int nCount )
{
for ( int i = 0; i < nCount; i++, pNewPersons++ )
{
// call CPerson default constructor directly
new( pNewPersons )CPerson;
}
}
This override iterates through the new CPerson objects, calling each object’s constructor. The special new operator used here constructs a new CPerson object in place rather than allocating memory from the heap.
Serializing Elements
The CArray, CList, and CMap classes call SerializeElements to store collection elements to or read them from an archive.
The default implementation of the SerializeElements helper function does a bitwise write from the objects to the archive, or a bitwise read from the archive to the objects, depending on whether the objects are being stored in or retrieved from the archive. Override SerializeElements if this action is not appropriate.
If your collection stores objects derived from CObject and you use the IMPLEMENT_SERIAL macro in the implementation of the collection element class, you can take advantage of the serialization functionality built into CArchive and CObject:
class CPerson : public CObject { . . . };
CArray< CPerson, CPerson& > personArray;
template <> void AFXAPI SerializeElements <CPerson> ( CArchive& ar, CPerson* pNewPersons, int nCount )
{
for ( int i = 0; i < nCount; i++, pNewPersons++ )
{
// Serialize each CPerson object
pNewPersons->Serialize( ar );
}
}
The overloaded insertion operators for CArchive call CObject::Serialize (or an override of that function) for each CPerson object.
Using Nontemplate Collection Classes
MFC also supports the collection classes introduced with MFC version 1.0. These classes are not based on templates. They can be used to contain data of the supported types CObject*, UINT, DWORD, and CString. You can use these predefined collections (such as CObList) to hold collections of any objects derived from CObject. MFC also provides other predefined collections to hold primitive types such as UINT and void pointers (void*). In general, however, it is often useful to define your own type-safe collections to hold objects of a more specific class and its derivatives. Note that doing so with the collection classes not based on templates is more work than using the template-based classes.
There are two ways to create type-safe collections with the nontemplate collections:
Use the nontemplate collections, with type casting if necessary. This is the easier approach.
Derive from and extend a nontemplate type-safe collection.
To use the nontemplate collections with type casting
Use one of the nontemplate classes, such as CWordArray, directly.
For example, you can create a CWordArray and add any 32-bit values to it, then retrieve them. There is nothing more to do. You just use the predefined functionality.
You can also use a predefined collection, such as CObList, to hold any objects derived from CObject. A CObList collection is defined to hold pointers to CObject. When you retrieve an object from the list, you may have to cast the result to the proper type since the CObList functions return pointers to CObject. For example, if you store
CPerson
objects in a CObList collection, you have to cast a retrieved element to be a pointer to aCPerson
object. The following example uses a CObList collection to holdCPerson
objects:class CPerson : public CObject {...}; CPerson* p1 = new CPerson(...); CObList myList; myList.AddHead( p1 ); // No cast needed CPerson* p2 = ( CPerson* )myList.GetHead();
This technique of using a predefined collection type and casting as necessary may be adequate for many of your collection needs. If you need further functionality or more type safety, use a template-based class, or follow the next procedure.
To derive and extend a nontemplate type-safe collection
Derive your own collection class from one of the predefined nontemplate classes.
When you derive your class, you can add type-safe wrapper functions to provide a type-safe interface to existing functions.
For example, if you derived a list from CObList to hold
CPerson
objects, you might add the wrapper functionsAddHeadPerson
andGetHeadPerson
, as shown below.class CPersonList : public CObList { public: void AddHeadPerson( CPerson* person ) {AddHead( person );} const CPerson* GetHeadPerson() {return (CPerson*)GetHead();} };
These wrapper functions provide a type-safe way to add and retrieve
CPerson
objects from the derived list. You can see that for theGetHeadPerson
function, you are simply encapsulating the type casting.You can also add new functionality by defining new functions that extend the capabilities of the collection rather than just wrapping existing functionality in type-safe wrappers. For example, the article Collections: Deleting All Objects in a CObject Collection describes a function to delete all the objects contained in a list. This function could be added to the derived class as a member function.