Storing Containers in Containers

Problem

You have a number of instances of a standard container (lists, sets, etc.), and you want to keep track of them by storing them in yet another container.

Solution

Store pointers to your containers in a single, master container. For example, you can use a map to store a string key and a pointer to a set as its value. Example 6-12 presents a simple transaction log class that stores its data as a map of string-set pointer pairs.

Example 6-12. Storing set pointers in a map

#include #include #include #include using namespace std; typedef set SetStr; typedef map MapStrSetStr; // Dummy database class class DBConn { public: void beginTxn( ) {} void endTxn( ) {} void execSql(string& sql) {} }; class SimpleTxnLog { public: SimpleTxnLog( ) {} ~SimpleTxnLog( ) {purge( );} // Add an SQL statement to the list void addTxn(const string& id, const string& sql) { SetStr* pSet = log_[id]; // This creates the entry for if (pSet == NULL) { // this id if it isn't there pSet = new SetStr( ); log_[id] = pSet; } pSet->insert(sql); } // Apply the SQL statements to the database, one transaction // at a time void apply( ) { for (MapStrSetStr::iterator p = log_.begin( ); p != log_.end( ); ++p) { conn_->beginTxn( ); // Remember that a map iterator actually refers to an object // of pair. The set pointer is stored in p->second. for (SetStr::iterator pSql = p->second->begin( ); pSql != p->second->end( ); ++pSql) { string s = *pSql; conn_->execSql(s); cout << "Executing SQL: " << s << endl; } conn_->endTxn( ); delete p->second; } log_.clear( ); } void purge( ) { for (MapStrSetStr::iterator p = log_.begin( ); p != log_.end( ); ++p) delete p->second; log_.clear( ); } // ... private: MapStrSetStr log_; DBConn* conn_; };

 

Discussion

Example 6-12 offers one situation where you might need to store containers within a container. Imagine that you need to store a series of SQL statements in batches, to be executed against a relational database all at once sometime in the future. That's what SimpleTxnLog does. It could stand to have a few more member functions to make it useful, and some exception handling to make it safe, but the purpose of the example is to show how to store one kind of container in another.

To begin with, I created some typedefs to make the code easier to read:

typedef std::set SetStr; typedef std::map MapStrSetStr;

When you are using templates of templates (of templates . . . ad nauseam), the declarations will get very long, which makes them hard to read, so make your life easier by employing typedef. Furthermore, using typedef makes it easier to change something about the template declaration without having to search and replace through multiple source files.

The DBConn class is a dummy class that is supposed to represent a connection to a relational database. The interesting part comes when we get into the definition of SimpleTxnLog, in the addTxn member function. At the beginning of the function, I do this to see if there is already a set object for the id that was passed in:

SetStr* pSet = log_[id];

log_ is a map (see Recipe 6.6), so operator[] does a lookup of id to see if there is a data object associated with it. If there is, the data object is returned and pSet is non-NULL; if there isn't, it creates it and returns the associated pointer, which will be NULL. Then, I can check to see if pSet points to anything to determine if I need to create another set:

if (pSet == NULL) { pSet = new SetStr( ); // SetStr = std::set log_[id] = pSet; }

Once I create the set, I have to assign it back to the associated key in the map, since pSet is a copy of the data object stored in the map (a set pointer), not the value itself. Once I do that, all that's left is to add an element to the set and return:

pSet->insert(sql);

With the above steps, I added a pointer to an address of one container (a set) to another (a map). What I didn't do was add a set object to a map. The difference is important. Since containers have copy-in, copy-out semantics, doing the following would copy the entire set s into the map:

set s; // Load up s with data... log_[id] = s; // Copy s and add the copy to log_

This will cause a lot of extra copying that you probably don't want. Therefore, the general rule to follow when using containers of containers is to use containers of pointers to containers.

Категории