w Inquiry.com - answers for IT professionals
 


 
Ask the C++ Pro 10-Minute Solutions

Creating Heterogeneous Containers
By Danny Kalev

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 true—STL 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.

 
Other 10-Minute Solutions
 How to Change the Mouse Pointer without Flicker
 Setting Full Row Selection in ListView Control
 Automating Type Conversions with stringstream Objects
 Improving Memory Reallocation with Vectors
 How to Use <fstream> Classes for File I/O
 Casting About for Safe Typecasting
 Overloading Operator + the Right Way
 How to Create Persistent Objects
 Making Linked Lists More User-Friendly
 Preventing Glitches in Signal Processing
 Forcing Object Allocation on the Free-store
 Using String-Based Data Validation
 Implementing the 'Resource Acquisition Is Initialization' Idiom
 Simple Locks for Data Files
 Template Specializations
 Exception Handling
 Using Bit Fields in Data Optimization
 Using the Transform() Algorithm to Change a String's Case
 Use RTTI for Dynamic Type Identification
 Choosing the Right swap () Implementation
 Take Charge and Initialize Your Own Data
 Share Data Among Objects Using the Monostate Design Pattern
 String Manipulation Made Easy with std::string Algorithms
 Using typedef to Curb Miscreant Code
 Managing Objects' Construction Order
 Bitwise Operators: Combining Efficiency and Ease of Use
 Use Function Adapters to Extend Generic Algorithms' Usage
 Simplify Callback Dispatching with Enumerated Indexes
 Streamline Your Bulk I/O Operations with Stream Iterators
 Optimize Your Member Layout
 Preserve Code Safety with Conversion Operators
 Modify Your Base Class Interface in Derived Classes
 Tackle Common Programming Tasks Using the New <tuple> Library
 Use Local Classes for Proper Cleanup in Exception-enabled Apps
 Use multimap to Create Associative Containers with Duplicate Keys
 Enforcing Compile-time Constraints
 Facilitate Directory Operations with the <dirent.h> and <dir.h> Libraries
 Spruce Up Your Built-in Arrays
 Target 32- and 64-bit Platforms Together with a Few Simple Datatype Changes
 Restrict Object Allocation to Specific Memory Types
 Use the Pimpl Idiom to Reduce Compilation Time and Enhance Encapsulation
 Automate Resource Management with shared_ptr
 The Quick and Dirty Way to Add
 Pointing to Class Members
 Detecting Keystrokes While Your Application is Busy
 Linked Lists
 Programming the System Tray
 Create a "Universal" DLL
 Convert Path to Long Path Name
 Constructing an Object at a Pre-Determined Memory Position
 Declaring Classes and Member Functions in a Namespace
 Using the auto_ptr Class Template to Facilitate Dynamic Memory Management
 Using the random_shuffle() Algorithm to Randomize a Sequence of Elements
 Defining a Function Object
 Implementing the Singleton Design Pattern
 Declaring Function Pointers and Implementing Callbacks
 Overloading Operator << for a User-Defined Type
 Implementing a Stopwatch Class for Performance Measurements
 Creating and Accessing Environment Variables
 Executing an Object's Member Function in a Separate Thread
 Creating Heterogeneous Containers
 Overriding New and Delete
 Time and Date Manipulation
  Defining Functions with a Variable Argument List
 Optimize Abstract Operations with Function Templates




Sponsored Links


Advertising Info  |   Member Services  |   Contact Us  |   Help  |   Feedback  |   Site Map
Jupiterweb networks

internet.comearthweb.comDevx.comClickZ

Search Jupiterweb:

Jupitermedia Corporation has four divisions:
JupiterWeb, JupiterResearch, JupiterEvents, and JupiterImages

Copyright 2004 Jupitermedia Corporation All Rights Reserved.
Legal Notices, Licensing, Reprints, & Permissions, Privacy Policy.

Jupitermedia Corporate Info | Newsletters | Tech Jobs | E-mail Offers