The Problem
Many users are surprised when they hear that the C++ Standard Template Library (STL) is not an object-oriented framework. However, this is trueSTL was designed as a generic framework rather than an object-oriented one. One of the consequences of this is that STL doesn't allow you to store objects of different types as elements of the same container. In other object-oriented languages such as Smalltalk, you can store different types of objects in the same container. These are called heterogeneous containers. Heterogeneous containers can be useful in certain programming tasks. For instance, suppose you need to store various multimedia files, .wav, .mp3, and .ra in the favorites list of a media player. It would be handy to represent the list as an STL vector of a multimedia objects, each of which contains the physical location of the multimedia file, its size, creation date, and additional information about the clip as follows:
class mutimedia_file {
public:
int virtual play();
int virtual pause();
int virtual rewind();
int virtual ffwd();
};
class wav_file : public mutimedia_file {/*..*/};
class mp3_file : public mutimedia_file {/*..*/};
class ra_file : public mutimedia_file {/*..*/};
int main
{
std::vector <mutimedia_file> favorites;
mp3_file mp3_clip("oops! I did it again");
ra_file ra_clip("American pie");
favorites.push_back(mp3_clip); // undefined behavior
favorites.push_back(ra_clip); // undefined behavior
}
Unfortunately, you can't store a derived object in an STL container of a base type because the derived object might be sliced, thereby resulting undefined behavior.
Storing Pointers as Elements
Because STL containers have value semantics i.e., they store the actual object as an element rather than storing a reference or pointer, slicing may occur when you attempt to store derived objects. Furthermore, even if a derived class and its base class occupy the same size, virtual member functions are not resolved dynamically:
#include vector>
using namespace std;
int main()
{
vector <multimedia_file> v; // value semantics
mp3_file clip;
v.push_back(&clip);
v[0].play(); // actually calls multimedia_file::play(),
//not mp3_file::play()
}
To overcome both problems, we store pointers as elements of the container rather than objects. Obviously, the pointers must refer to exiting objects whose lifetime is in sync with the container's lifetime. Therefore, these objects must be allocated on the free-store using new. This way, you avoid the risk of having dangling pointers in the container. Consider:
int f(vector <multimedia_file *> & v)
{
mp3_file mp3_clip("crazy") ;
v.push_back(&mp3_clip); // bad idea
}// mp3_clip is destroyed here, v holds a dangling pointer!
As a rule, I recommend that you create your objects on the stack whenever possible and minimize the use of dynamic memory allocation. However, heterogeneous containers are one of the rare cases in which dynamic allocation is still the best strategy. To fill a heterogeneous container with pointers to different objects, simply pass the result of a new expression to the appropriate member function. For example:
void fill(vector < multimedia_file *> & v)
{
v.push_back( new mp3_file ("baby one more time") ;
v.push_back( new wav_file ("email_alert") ;
);
The pointers inserted into v are of different types: one is of type mp3_file* and the second is of type wav_file* (note: for brevity and simplicity, the code above doesn't check for memory exhaustion exceptions). By passing the pointer returned by new directly into the container instead of using a named variable, you ensure that the object can only be accessed through the container. This minimizes the likelihood of dangling pointers.
Accessing the Stored Elements
You access the objects whose pointers are stored in the container as you would access an ordinary element. The only difference is that you use the -> notation. For example:
// using an iterator
vector < multimedia_file * >::iterator it=v.begin();
(*it)->play(); // calls mp3_file::play()
// using the subscript operator
v[1]->play(); // play "email_alert" .wav file
Because we access the member functions through pointers, we ensure that virtual functions are resolved dynamically according to the dynamic type of their object.
Destroying the Container
Once you're done with the heterogeneous container, you must destroy its elements manually. This is because the elements were allocated on the free-store and the container doesn't own them:
// destroying the elements manually
vector < multimedia_file *>::iterator it=v.begin();
for (int i=0; it < v.end(); ++i)
{
delete v[i];
}
The elements' destruction must take place just before the container itself is destroyed. Otherwise you will cause a memory leak.
Additional Considerations
Remember that STL requires that its elements be "copy-constructible" and "assignable." These fancy terms of the C++ standard basically mean that copying such an object is a well-behaved operation that doesn't destroy or invalidate the source object in any other way . Unfortunately, std::auto_ptr doesn't meet these criteria. Therefore, don't be tempted to wrap the pointers in auto_ptr objects.
Although STL doesn't support polymorphism directly, the technique I've presented enables you to overcome this limitation and create a heterogeneous container while benefiting from the advantage of STL's high-performance and simplicity.