Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Professional C++ [eng].pdf
Скачиваний:
1715
Добавлен:
16.08.2013
Размер:
11.09 Mб
Скачать

Delving into the STL: Containers and Iterators

#include <stack> #include <stdexcept>

// Details of Error class omitted for brevity

//

// Simple ErrorCorrelator class that returns most recent errors first

//

class ErrorCorrelator

{

public: ErrorCorrelator() {}

//

// Add an error to be correlated.

//

void addError(const Error& error);

//

// Retrieve the next error to be processed.

//

Error getError() throw (std::out_of_range);

protected:

std::stack<Error> mErrors;

private:

// Prevent assignment and pass-by-reference. ErrorCorrelator(const ErrorCorrelator& src); ErrorCorrelator& operator=(const ErrorCorrelator& rhs);

};

Associative Containers

Unlike the sequential containers, the associative containers do not store elements in a linear configuration. Instead, they provide a mapping of keys to values. They generally offer insertion, deletion, and lookup times that are equivalent to each other.

The four associative containers provided by the STL are map, multimap, set, and multiset. Each of these containers stores its elements in a sorted, treelike, data structure.

The pair Utility Class

Before learning about the associative containers, you must become familiar with the pair class, which is defined in the <utility> header file. pair is a class template that groups together two values of possibly different types. The values are accessible through the first and second public data members. operator== and operator< are defined for pairs to compare both the first and second elements. Here are some examples:

#include <utility> #include <string> #include <iostream>

595

Chapter 21

using namespace std;

int main(int argc, char** argv)

{

//Two-argument ctor and default ctor pair<string, int> myPair(“hello”, 5), myOtherPair;

//Can assign directly to first and second myOtherPair.first = “hello”; myOtherPair.second = 6;

//Copy ctor.

pair<string, int> myThirdPair(myOtherPair);

// operator<

if (myPair < myOtherPair) {

cout << “myPair is less than myOtherPair\n”; } else {

cout << “myPair is greater than or equal to myOtherPair\n”;

}

// operator==

if (myOtherPair == myThirdPair) {

cout << “myOtherPair is equal to myThirdPair\n”; } else {

cout << “myOtherPair is not equal to myThirdPair\n”;

}

return (0);

}

The library also provides a utility function template, make_pair(), that constructs a pair from two variables. For example, you could use it like this:

pair<int, int> aPair = make_pair(5, 10);

Of course, in this case you could have just used the two-argument constructor. However, make_pair() is more useful when you want to pass a pair to a function. Unlike class templates, function templates can infer types from parameters, so you can use make_pair() to construct a pair without explicitly specifying the types.

Using pointer types in pairs is risky because the pair copy constructor and assignment operator perform only shallow copies and assignments of pointer types.

map

The map is one of the most useful containers. It stores key/value pairs instead of just a single value. Insertion, lookup, and deletion are all based on the key; the value is just “along for the ride.” The term “map” comes from the conceptual understanding that the container “maps” keys to values. You might be more familiar with the concept of a hash table. The map provides a similar interface; the differences are in the underlying data structure and the algorithmic complexity of the operations.

596

Delving into the STL: Containers and Iterators

The map keeps elements in sorted order, based on the keys, so that insertion, deletion, and lookup all take logarithmic time. It is usually implemented as some form of balanced tree, such as a red-black tree. However, the tree structure is not exposed to the client.

You should use a map whenever you need to store and retrieve elements based on a “key” value.

Constructing Maps

The map template takes four types: the key type, the value type, the comparison type, and the allocator type. As usual, we ignore the allocator in this chapter; see Chapter 23 for details. The comparison type is similar to the comparison type for priority_queue described above. It allows you to specify a different comparison class than the default. You usually shouldn’t need to change the sorting criteria. In this chapter, we use only the default less comparison. When using the default, make sure that your keys all respond to operator< appropriately.

If you’re interested in further detail, Chapter 22 explains how to write your own comparison classes.

If you ignore the comparison and allocator parameters (which we urge you to do), constructing a map is just like constructing a vector or list, except that you specify the key and value types separately in the template. For example, the following code constructs a map that uses ints as the key and stores objects of the Data class (whose full definition is not shown):

#include <map> using namespace std;

class Data

{

public:

Data(int val = 0) { mVal = val; } int getVal() const { return mVal; } void setVal(int val) {mVal = val; } // Remainder of definition omitted

protected: int mVal;

};

int main(int argc, char** argv)

{

map<int, Data> dataMap; return (0);

}

Inserting Elements

Inserting an element into the sequential containers such as vector and list always requires you to specify the position at which the element is to be added. The map, along with the other associative containers, is different. The map internal implementation determines the position in which to store the new element; you need only to supply the key and the value.

map and the other associative containers do provide a version of insert() that takes an iterator position. However, that position is only a “hint” to the container as to the correct position. The container is not required to insert the element at that position.

597

Chapter 21

When inserting elements, it is important to keep in mind that maps support so-called “unique keys:” every element in the map must have a different key. If you want to support multiple elements with the same key, you must use multimaps, which are described below.

There are two ways to insert an element into the map: one clumsy and one not so clumsy.

The insert() Method

The clumsy mechanism to add an element to a map is the insert() method. One problem is that you must specify the key/value pair as a pair object. The second problem is that the return value from the basic form of insert() is a pair of an iterator and a bool. The reason for the complicated return value is that insert() does not overwrite an element value if one already exists with the specified key. The bool element of the return pair specifies whether the insert() actually inserted the new key/ value pair. The iterator refers to the element in the map with the specified key (with a new or old value, depending on whether the insert succeeded or failed). Continuing the map example from the previous section, here is how to use insert():

#include <map> #include <iostream> using namespace std;

class Data

{

public:

Data(int val = 0) { mVal = val; } int getVal() const { return mVal; } void setVal(int val) {mVal = val; } // Remainder of definition omitted

protected: int mVal;

};

int main(int argc, char** argv)

{

map<int, Data> dataMap;

pair<map<int, Data>::iterator, bool> ret;

ret = dataMap.insert(make_pair(1, Data(4))); if (ret.second) {

cout << “Insert succeeded!\n”; } else {

cout << “Insert failed!\n”;

}

ret = dataMap.insert(make_pair(1, Data(6))); if (ret.second) {

cout << “Insert succeeded!\n”; } else {

cout << “Insert failed!\n”;

}

return (0);

}

598

Delving into the STL: Containers and Iterators

Note the use of make_pair() to construct the pair to pass to the insert() method. The output from the program is:

Insert succeeded!

Insert failed!

operator[]

The less clumsy way to insert an element into the map is through the overloaded operator[]. The difference is mainly in the syntax: you specify the key and value separately. Additionally, operator[] always succeeds. If no element value with the given key exists, it creates a new element with that key and value. If an element with the key exists already, operator[] replaces the element value with the newly specified value. Here is the previous example using operator[] instead of insert():

#include <map> #include <iostream> using namespace std;

class Data

{

public:

Data(int val = 0) { mVal = val; } int getVal() const { return mVal; } void setVal(int val) {mVal = val; } // Remainder of definition omitted

protected: int mVal;

};

int main(int argc, char** argv)

{

map<int, Data> dataMap; dataMap[1] = Data(4);

dataMap[1] = Data(6); // Replaces the element with key 1 return (0);

}

There is, however, one major caveat to operator[]: it always constructs a new value object, even if it doesn’t need to use it. Thus, it requires a default constructor for your element values, and can be less efficient than insert().

Map Iterators

map iterators work similarly to the iterators on the sequential containers. The major difference is that the iterators refer to key/value pairs instead of just the values. In order to access the value, you must retrieve the second field of the pair object. Here is how you can iterate through the map from the previous example:

#include <map> #include <iostream> using namespace std;

class Data

{

599

Chapter 21

public:

Data(int val = 0) { mVal = val; } int getVal() const { return mVal; } void setVal(int val) {mVal = val; } // Remainder of definition omitted

protected: int mVal;

};

int main(int argc, char** argv)

{

map<int, Data> dataMap;

dataMap[1] = Data(4);

dataMap[1] = Data(6); // Replaces the element with key 1

for (map<int, Data>::iterator it = dataMap.begin(); it != dataMap.end(); ++it) {

cout << it->second.getVal() << endl;

}

return (0);

}

Take another look at the expression used to access the value:

it->second.getVal()

it refers to a key/value pair, so you can use the -> operator to access the second field of that pair, which is a Data object. You can then call the getVal() method on that data object.

Note that the following code is functionally equivalent:

(*it).second.getVal()

You still see a lot of code that like around because -> didn’t used to be required for iterators.

You can modify element values through non-const iterators, but you cannot modify the key of an element, even through a non-const iterator, because it would destroy the sorted order of the elements in the map.

map iterators are bidirectional.

Looking Up Elements

The map provides logarithmic lookup of elements based on a supplied key. If you already know that an element with a given key is in the map, the simplest way to look it up is through operator[]. The nice thing about operator[] is that it returns a reference to the element that you can use (or modify on a non-const map) directly, without worrying about pulling the value out of a pair object. Here is an extension to the preceding example to call the setVal() method on the Data object value at key 1:

600

Delving into the STL: Containers and Iterators

#include <map> #include <iostream> using namespace std;

class Data

{

public:

Data(int val = 0) { mVal = val; } int getVal() const { return mVal; } void setVal(int val) {mVal = val; } // Remainder of definition omitted

protected: int mVal;

};

int main(int argc, char** argv)

{

map<int, Data> dataMap; dataMap[1] = Data(4); dataMap[1] = Data(6); dataMap[1].setVal(100);

return (0);

}

However, if you don’t know whether the element exists, you may not want to use operator[], because it will insert a new element with that key if it doesn’t find one already. As an alternative, the map provides a find() method that returns an iterator referring to the element with the specified key, if it exists, or the end() iterator if its not in the map. Here is an example using find() to perform the same modification to the Data object with key 1:

#include <map> #include <iostream> using namespace std;

class Data

{

public:

Data(int val = 0) { mVal = val; } int getVal() const { return mVal; } void setVal(int val) {mVal = val; }

// Remainder of definition omitted protected:

int mVal;

};

int main(int argc, char** argv)

{

map<int, Data> dataMap; dataMap[1] = Data(4); dataMap[1] = Data(6);

601

Chapter 21

map<int, Data>::iterator it = dataMap.find(1); if (it != dataMap.end()) {

it->second.setVal(100);

}

return (0);

}

As you can see, using find() is a bit clumsier, but it’s sometimes necessary.

If you only want to know whether or not an element with a certain key is in the map, you can use the count() member function. It returns the number of elements in the map with a given key. For maps, the result will always be 0 or 1 because there can be no elements with duplicate keys. The following section shows an example using count().

Removing Elements

The map allows you to remove an element at a specific iterator position or to remove all elements in a given iterator range, in amortized constant and logarithmic time, respectively. From the client perspective, these two erase() methods are equivalent to those in the sequential containers. A great feature of the map, however, is that it also provides a version of erase() to remove an element matching a key. Here is an example:

//#includes, Data class definition, and beginning of main function omitted.

//See previous examples for details.

map<int, Data> dataMap; dataMap[1] = Data(4);

cout << “There are “ << dataMap.count(1) << “ elements with key 1\n”; dataMap.erase(1);

cout << “There are “ << dataMap.count(1) << “ elements with key 1\n”;

Map Example: Bank Account

You can implement a simple bank account database using a map. A common pattern is for the key to be one field of a class or struct that is stored in the map. In this case, the key is the account number. Here are simple BankAccount and BankDB classes:

#include <map> #include <string> #include <stdexcept> using std::map; using std::string;

using std::out_of_range;

class BankAccount

{

public:

BankAccount(int acctNum, const string& name) : mAcctNum(acctNum), mClientName(name) {}

void setAcctNum(int acctNum) { mAcctNum = acctNum; } int getAcctNum() const {return (mAcctNum); }

void setClientName(const string& name) { mClientName = name; } string getClientName() const { return mClientName; }

602

Delving into the STL: Containers and Iterators

// Other public methods omitted

protected:

int mAcctNum; string mClientName;

// Other data members omitted

};

class BankDB

{

public: BankDB() {}

//Adds acct to the bank database. If an account

//exists already with that number, the new account is

//not added. Returns true if the account is added, false

//if it’s not.

bool addAccount(const BankAccount& acct);

//Removes the account acctNum from the database void deleteAccount(int acctNum);

//Returns a reference to the account represented

//by its number or the client name.

//Throws out_of_range if the account is not found BankAccount& findAccount(int acctNum) throw(out_of_range);

BankAccount& findAccount(const string& name) throw(out_of_range);

//Adds all the accounts from db to this database.

//Deletes all the accounts in db.

void mergeDatabase(BankDB& db);

protected:

map<int, BankAccount> mAccounts;

};

Here are implementations of the BankDB methods:

#include “BankDB.h” #include <utility> using namespace std;

bool BankDB::addAccount(const BankAccount& acct)

{

//Declare a variable to store the return from insert(). pair<map<int, BankAccount>::iterator, bool> res;

//Do the actual insert, using the account number as the key. res = mAccounts.insert(make_pair(acct.getAcctNum(), acct));

//Return the bool field of the pair specifying success or failure. return (res.second);

}

void BankDB::deleteAccount(int acctNum)

{

603