Follow Techotopia on Twitter

On-line Guides
All Guides
eBook Store
iOS / Android
Linux for Beginners
Office Productivity
Linux Installation
Linux Security
Linux Utilities
Linux Virtualization
Linux Kernel
System/Network Admin
Scripting Languages
Development Tools
Web Development
GUI Toolkits/Desktop
Mail Systems
Eclipse Documentation

How To Guides
General System Admin
Linux Security
Linux Filesystems
Web Servers
Graphics & Desktop
PC Hardware
Problem Solutions




Thinking in C++ Vol 2 - Practical Programming
Prev Home Next

Associative containers

The set, map, multiset, and multimap are called associative containers because they associate keys with values. Well, at least maps and multimaps associate keys with values, but you can look at a set as a map that has no values, only keys (and they can in fact be implemented this way), and the same for the relationship between multiset and multimap. So, because of the structural similarity, sets and multisets are lumped in with associative containers.

The most important basic operations with associative containers are putting things in and, in the case of a set, seeing if something is in the set. In the case of a map, you want to first see if a key is in the map, and if it exists, you want the associated value for that key. There are many variations on this theme, but that s the fundamental concept. The following example shows these basics:

//: C07:AssociativeBasics.cpp {-bor}
// Basic operations with sets and maps.
//{L} Noisy
#include <cstddef>
#include <iostream>
#include <iterator>
#include <map>
#include <set>
#include "Noisy.h"
using namespace std;
int main() {
Noisy na[7];
// Add elements via constructor:
set<Noisy> ns(na, na + sizeof na/sizeof(Noisy));
Noisy n;
ns.insert(n); // Ordinary insertion
cout << endl;
// Check for set membership:
cout << "ns.count(n)= " << ns.count(n) << endl;
if(ns.find(n) != ns.end())
cout << "n(" << n << ") found in ns" << endl;
// Print elements:
copy(ns.begin(), ns.end(),
ostream_iterator<Noisy>(cout, " "));
cout << endl;
cout << "\n----------- << endl;
map<int, Noisy> nm;
for(int i = 0; i < 10; i++)
nm[i]; // Automatically makes pairs
cout << "\n----------- << endl;
for(size_t j = 0; j < nm.size(); j++)
cout << "nm[" << j <<"] = " << nm[j] << endl;
cout << "\n----------- << endl;
nm[10] = n;
cout << "\n----------- << endl;
nm.insert(make_pair(47, n));
cout << "\n----------- << endl;
cout << "\n nm.count(10)= " << nm.count(10) << endl;
cout << "nm.count(11)= " << nm.count(11) << endl;
map<int, Noisy>::iterator it = nm.find(6);
if(it != nm.end())
cout << "value:" << (*it).second
<< " found in nm at location 6" << endl;
for(it = nm.begin(); it != nm.end(); it++)
cout << (*it).first << ":" << (*it).second << ", ";
cout << "\n----------- << endl;
} ///:~

The set<Noisy> object ns is created using two iterators into an array of Noisy objects, but there is also a default constructor and a copy-constructor, and you can pass in an object that provides an alternate scheme for doing comparisons. Both sets and maps have an insert( ) member function to put things in, and you can check to see if an object is already in an associative container in two ways. The count( ) member function, when given a key, will tell you how many times that key occurs. (This can only be zero or one in a set or map, but it can be more than one with a multiset or multimap.) The find( ) member function will produce an iterator indicating the first occurrence (with set and map, the only occurrence) of the key that you give it or will produce the past-the-end iterator if it can t find the key. The count( ) and find( ) member functions exist for all the associative containers, which makes sense. The associative containers also have member functions lower_bound( ), upper_bound( ), and equal_range( ), which only make sense for multiset and multimap, as you will see. (But don t try to figure out how they would be useful for set and map, since they are designed for dealing with a range of duplicate keys, which those containers don t allow.)

Designing an operator[ ] always presents a bit of a dilemma. Because it s intended to be treated as an array-indexing operation, people don t tend to think about performing a test before they use it. But what happens if you decide to index out of the bounds of the array? One option is to throw an exception, but with a map, indexing out of the array could mean that you want to create a new entry at that location, and that s the way the STL map treats it. The first for loop after the creation of the map<int, Noisy> nm just looks up objects using the operator[ ], but this is actually creating new Noisy objects! The map creates a new key-value pair (using the default constructor for the value) if you look up a value with operator[ ] and it isn t there. This means that if you really just want to look something up and not create a new entry, you must use the member functions count( ) (to see if it s there) or find( ) (to get an iterator to it).

A number of problems are associated with the for loop that prints the values of the container using operator[ ]. First, it requires integral keys (which we happen to have here). Next and worse, if all the keys are not sequential, you ll end up counting from zero to the size of the container, and if some spots don t have key-value pairs, you ll automatically create them and miss some of the higher values of the keys. Finally, if you look at the output from the for loop, you ll see that things are very busy, and it s quite puzzling at first why there are so many constructions and destructions for what appears to be a simple lookup. The answer only becomes clear when you look at the code in the map template for operator[ ], which will be something like this:

mapped_type& operator[] (const key_type& k) {
value_type tmp(k,T());
return (*((insert(tmp)).first)).second;

The map::insert( ) function takes a key-value pair and does nothing if there is already an entry in the map with the given key otherwise it inserts an entry for the key. In either case, it returns a new key-value pair holding an iterator to the inserted pair as its first element and holding true as the second element if an insertion took place. The members first and second give the key and value, respectively, because map::value_type is really just a typedef for a std::pair:

typedef pair<const Key, T> value_type;

You ve seen the std::pair template before. It s a simple holder for two values of independent types, as you can see by its definition:

template<class T1, class T2> struct pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair(const T1& x, const T2& y) : first(x), second(y) {}
// Templatized copy-constructor:
template<class U, class V> pair(const pair<U, V> &p);

The pair template class is very useful, especially when you want to return two objects from a function (since a return statement only takes one object). There s even a shorthand for creating a pair called make_pair( ), which is used in AssociativeBasics.cpp.

So to retrace the steps, map::value_type is a pair of the key and the value of the map actually, it s a single entry for the map. But notice that pair packages its objects by value, which means that copy-constructions are necessary to get the objects into the pair. Thus, the creation of tmp in map::operator[ ] will involve at least a copy-constructor call and destructor call for each object in the pair. Here, we re getting off easy because the key is an int. But if you want to really see what kind of activity can result from map::operator[ ], try running this:

//: C07:NoisyMap.cpp
// Mapping Noisy to Noisy.
//{L} Noisy
#include <map>
#include "Noisy.h"
using namespace std;
int main() {
map<Noisy, Noisy> mnn;
Noisy n1, n2;
cout << "\n-------- << endl;
mnn[n1] = n2;
cout << "\n-------- << endl;
cout << mnn[n1] << endl;
cout << "\n-------- << endl;
} ///:~

You ll see that both the insertion and lookup generate a lot of extra objects, and that s because of the creation of the tmp object. If you look back up at map::operator[ ], you ll see that the second line calls insert( ), passing it tmp that is, operator[ ] does an insertion every time. The return value of insert( ) is a different kind of pair, where first is an iterator pointing to the key-value pair that was just inserted, and second is a bool indicating whether the insertion took place. You can see that operator[ ] grabs first (the iterator), dereferences it to produce the pair, and then returns the second, which is the value at that location.

So on the upside, map has this fancy make a new entry if one isn t there behavior, but the downside is that you always get a lot of extra object creations and destructions when you use map::operator[ ]. Fortunately, AssociativeBasics.cpp also demonstrates how to reduce the overhead of insertions and deletions, by avoiding operator[ ] if you don t need it. The insert( ) member function is slightly more efficient than operator[ ]. With a set, you hold only one object, but with a map, you hold key-value pairs; so insert( ) requires a pair as its argument. Here s where make_pair( ) comes in handy, as you can see.

For looking objects up in a map, you can use count( ) to see whether a key is in the map, or you can use find( ) to produce an iterator pointing directly at the key-value pair. Again, since the map contains pairs, that s what the iterator produces when you dereference it, so you have to select first and second. When you run AssociativeBasics.cpp, you ll notice that the iterator approach involves no extra object creations or destructions. It s not as easy to write or read, though.

Thinking in C++ Vol 2 - Practical Programming
Prev Home Next

   Reproduced courtesy of Bruce Eckel, MindView, Inc. Design by Interspire