Chapter 6
IN THIS CHAPTER
Architecting the Standard C++ Library
Managing data in vector, map, list, or set
Stacking and queuing
Interacting with dynamic arrays and unordered data
When you get around in the world of C++ programming, you encounter two different libraries that people use to make their lives easier. These two libraries are:
In this case, library means a set of classes that you can use in your applications. These libraries include handy classes, such as string
and vector
(which is like an array — it’s a list you use to store objects).
The difference between the Standard C++ Library and STL is that STL came first. STL was used by so many developers that the American National Standards Institute (ANSI) decided to standardize it. The result is the similar Standard C++ Library that is part of the official ANSI standard and now part of most modern C++ compilers. This chapter uses the Standard C++ Library, or simply the Standard Library. The concepts presented here also apply to STL, so if you’re using STL, you can use this chapter.
When people start using the Standard Library, they often ask about the source code. They see the header files, but no .cpp
files. There are no .cpp
files. ANSI architected the Standard Library for ease of use and reliability.
The classes contain their functions inside the class definitions; there are no forward declarations. You don’t add source files to your project or link in compiled libraries. Just add an include line for the libraries you want.
To see how this works for yourself, open any project file you’re worked on to date that has #include <iostream>
and relies on a cout
/endl
combination to output text. Right-click endl
and choose Find Implementation of: ’endl’ from the context menu. Code::Blocks will open the ostream
file and take you to the implementation of endl
, which basically outputs ’\n’
to the output stream, amid some other confusing code. When you scroll to the beginning of that file, you see:
/** @file include/ostream
* This is a Standard C++ Library header.
*/
There is no .cpp
file involved. All of the code appears in the header.
Computers need a place to store objects, so the Standard Library includes containers in which you can put objects. These special containers are called container classes, and the Standard Library implements them as templates. When you create an instance of a container class, you specify what class it holds.
The Vectors
example, shown in Listing 6-1, demonstrates how to use a container class. This particular container is a data type called a vector
, and it works much like an array.
LISTING 6-1: Using Vectors as Examples of Container Classes
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<string> names;
names.push_back("Tom");
names.push_back("Dick");
names.push_back("Harry");
names.push_back("April");
names.push_back("May");
names.push_back("June");
cout << names[0] << endl;
cout << names[5] << endl;
return 0;
}
You use vector
as a template. That means that it’s going to have a template parameter, which is string
in this case. Note also the included header files. Among them are <vector>
(with no .h
after the filename). In general, you include the header file that matches the name of the container you are using. Thus, if there were such a thing as a container called rimbucklebock
, you would type #include <rimbucklebock>
. Or, if you use the container called set
, you type #include <set>
. When you run this example, you see two names as output:
Tom
June
vector
, as you do with a dynamically defined array.vector
, so you don’t need to pass the size of the vector
to a function.vector
is filled, the underlying code allocates additional memory automatically.vector
from a function. To return an array, you must dynamically define it first.vector
directly.Here are some things you can do with vector
:
The Vectors2
example, shown in Listing 6-2, demonstrates how to use multiple vector
s in a single application. You can see that each one holds a different type, specified in the template parameter. This example requires C++ 11 or above to use.
LISTING 6-2: Creating More Advanced Vectors
#include <iostream>
#include <vector>
using namespace std;
class Employee {
public:
string Name;
string FireDate;
int GoofoffDays;
Employee(string aname, string afiredate,
int agoofdays) : Name(aname), FireDate(afiredate),
GoofoffDays(agoofdays) {}
};
int main() {
// A vector that holds strings
vector<string> MyAliases;
MyAliases.push_back(string("Bud The Sailor"));
MyAliases.push_back(string("Rick Fixit"));
MyAliases.push_back(string("Bobalou Billow"));
for (auto entry : MyAliases)
cout << entry << endl;
// A vector that holds integers
vector<int> LuckyNumbers;
LuckyNumbers.push_back(13);
LuckyNumbers.push_back(26);
LuckyNumbers.push_back(52);
for (auto entry : LuckyNumbers)
cout << entry << endl;
// A vector of default constructed ints.
vector<int> Default(5);
int i = 0;
vector<int>::reverse_iterator rentry = Default.rbegin();
for (; rentry != Default.rend(); rentry++)
*rentry = ++i;
for (auto entry : Default)
cout << entry << endl;
// A vector that holds Employee instances
vector<Employee> GreatWorkers;
GreatWorkers.push_back(Employee("George","123100", 50));
GreatWorkers.push_back(Employee("Tom","052002", 40));
for (auto entry : GreatWorkers)
cout << entry.Name << endl;
return 0;
}
After you compile and run this application, you see the following output from the cout
statements:
Bud The Sailor
Rick Fixit
Bobalou Billow
13
26
52
5
4
3
2
1
George
Tom
The Default
portion of the example is also interesting in that it declares a vector
of a specific size and reverse-fills the vector from the end to the beginning. Consequently, Default[0]
contains the value 5
, rather than 1
, as you might expect. To work backward, you must use a standard for
loop.
Sometimes you need a fixed-size array, but without the limitations of the built-in array. In this case, std::array
may do the trick for you. It provides built-in functionality, such as knowing its own size, supporting assignment, and providing random access iterators. The StdArray
example, shown in Listing 6-3, demonstrates two interesting ways that you can use std::array
. You need C++ 17 or above to use this example.
LISTING 6-3: Working with std::array to Overcome array Limitations
#include <iostream>
#include <array>
#include <algorithm>
#include <iterator>
using namespace std;
int main() {
array<char, 5> Letters = {'a', 'b', 'c', 'd', 'e'};
for (entry: Letters)
cout << entry << endl;
reverse_copy(Letters.begin(), Letters.end(),
ostream:iterator<char>(cout, " "));
return 0;
}
Notice that the declaration begins by providing the array type, char
, and the number of array elements, 5
, as template parameters. Letters
contains five char
values from ’a’
through ’e’
.
As shown in the example, you can use a standard iterated for
loop to display the individual entries. However, you can also output the Letter content in other ways, such as by performing a reverse_copy()
to the console with the ostream:iterator<char>()
. The point is to avoid limiting yourself to one coding style when another will do the job better with fewer lines. Here’s the output you see from the example:
a
b
c
d
e
e d c b a
The Maps
example, shown in Listing 6-4, demonstrates a type of container called a map
. A map
works much the same as a vector
, except for one main difference: You look up items in vector
by putting a number inside brackets, like this:
cout << names[0] << endl;
But with a map
, you can use any class or type you want for the index (called a key), not just numbers. To create an entry, you use a key (the index) and a value (the data) as a pair.
LISTING 6-4: Associating Objects with map
#include <iostream>
#include <map>
using namespace std;
int main() {
map<string, string> marriages;
marriages["Tom"] = "Suzy";
marriages["Harry"] = "Harriet";
cout << marriages["Tom"] << endl;
cout << marriages["Harry"] << endl;
return 0;
}
To use map
, you declare a variable of class map
, supplying two template parameters, the key class and the value class, which are both string
in the example. To store a map
value, you place a key inside brackets and set it equal to a value:
marriages["Tom"] = "Suzy";
To retrieve that particular item, you supply the key in brackets:
cout << marriages["Tom"] << endl;
When you run this example, you see the following two strings as output:
Suzy
Harriet
One of the most common discussions you encounter when people start talking about how to use the container templates is whether to put instances in the containers, pointers, or references. For example, which of the following should you type?
vector<MyClass>
vector<MyClass *>
vector<MyClass &>
In other words, do you want your container to store the actual instance (whatever that might mean), a reference to the actual instance, or a pointer to the instance? To explore this idea, look at the Maps2
example in Listing 6-5. Here, you’re trying out the different ways of storing things in map
: instances, pointers, and references.
LISTING 6-5: Making Decisions: Oh, What to Store?
#include <iostream>
#include <map>
using namespace std;
class StoreMe {
public:
int Item;
};
bool operator < (const StoreMe & first,
const StoreMe & second) {
return first.Item < second.Item;
}
int main() {
// First try storing the instances
map<StoreMe, StoreMe> instances;
StoreMe key1 = {10}; // braces notation!
StoreMe value1 = {20};
StoreMe key2 = {30};
StoreMe value2 = {40};
instances[key1] = value1;
instances[key2] = value2;
value1.Item = 12345;
cout << instances[key1].Item << endl;
instances[key1].Item = 34567;
cout << instances[key1].Item << endl;
// Next try storing pointers to the instances
map<StoreMe*, StoreMe*> pointers;
StoreMe key10 = {10};
StoreMe value10 = {20};
StoreMe key11 = {30};
StoreMe value11 = {40};
pointers[&key10] = &value10;
pointers[&key11] = &value11;
value10.Item = 12345;
cout << (*pointers[&key10]).Item << endl;
// Finally try storing references to the instances.
// Commented out because it causes an error.)
// map<StoreMe&, StoreMe&> pointers;
return 0;
}
To create the instances of StoreMe
, you use the braces notation. You can do that when you have no constructors. So the line
StoreMe key1 = {10};
creates an instance of StoreMe
and puts 10
in the Item
property. To create an individual instances
entry, you need both a key and a value instance of StoreMe
. You then use the key to provide a name for the value stored in the map
. Consequently, instances
contains two entries consisting of two StoreMe
objects each. Here’s what you see when you run the application:
20
34567
12345
This output doesn’t precisely match expectations because the code changes the Item
property in value1
:
value1.Item = 12345;
When the code outputs the value of instances[key1].Item
, you see an output of 20
, not 12345
. That means that the value stored in map
is a copy, not the original. However, when the code changes the value in instances
like this:
instances[key1].Item = 34567;
Now that you’ve figured out that map
is storing copies of what you put in it, the idea of storing a pointer should be clear: If you have a pointer variable and then you make a copy of it, although you have a separate pointer variable, the original and the copy both point to the same memory location. That’s the idea behind the second part of Listing 6-5. You create pointers
like this:
map<StoreMe*, StoreMe*> pointers;
Now this map
stores pointer variables. Remember that a pointer variable just holds a number that represents an address. If two separate pointer variables hold the same number, it means that they point to the same object at the same address. Furthermore, because this map
is holding pointers, it’s holding numbers, not instances — something to think about. To store a value when using pointers, you need to use code like this:
pointers[&key10] = &value10;
Note the use of the ampersand (&
) used as a reference operator to store addresses in map
. It’s now possible to change the Item
member of one the value objects:
value10.Item = 12345;
that you print using this carefully parenthesized line:
cout << (*pointers[&key10]).Item << endl;
and you see this:
12345
Don’t worry just now about the bool operator < (const StoreMe & first, const StoreMe & second)
function. This function is explained in the “Performing comparisons” section, later in this chapter.
Note that the following line is commented out:
// map<StoreMe&, StoreMe&> pointers;
It attempts to declare a map
that holds references, but the code generates a compiler error instead. Try uncommenting the commented line and see the error message. Here’s an example of what you might see (make sure to add the comment back in when you’re done):
error: conflicting declaration 'std::map<StoreMe&,
StoreMe&> pointers'
error: 'pointers' has a previous declaration as
'std::map<StoreMe*, StoreMe*> pointers'
All C++ containers, not just maps, generally make copies of whatever you stick inside them as shown in the previous section. The Vectors3
example, shown in Listing 6-6, replicates the essential functionality of the Maps2
example shown in Listing 6-5.
LISTING 6-6: The vector Version of the Maps2 Example
#include <iostream>
#include <vector>
using namespace std;
class StoreMe {
public:
int Item;
};
int main() {
vector<StoreMe> instances;
StoreMe value1 = {20};
StoreMe value2 = {40};
instances.push_back(value1);
instances.push_back(value2);
value1.Item = 12345;
cout << instances[0].Item << endl;
instances[0].Item = 34567;
cout << instances[0].Item << endl;
vector<StoreMe*> pointers;
StoreMe value10 = {20};
StoreMe value11 = {40};
pointers.push_back(& value10);
pointers.push_back(& value11);
value10.Item = 12345;
cout << (*pointers[0]).Item << endl;
return 0;
}
Oddly enough, the output from this example is precisely the same as the Maps2
example and for the same reason. Whether the container is a vector
or a map
doesn’t matter; both of them hold copies of the objects or pointers you provide. Consequently, you can remember these two rules about deleting your original objects:
It’s up to you to decide which method is better. But here are a couple of things to consider:
When you work with classes that contain other classes (such as vector
), you need to provide the class with a way to compare two things. The following sections describe how to provide comparison capability when working with containers.
For humans comparing is easy, but it’s not that easy for a computer. For example, suppose you have two pointers to string
objects. The first points to a string
containing abc
. The second points to another string
containing abc
. When writing code, you must consider whether the two variables are equal:
string
objects are equal.string
objects aren’t equal.Now look at this code:
string *pointer3 = new string("abc");
string *pointer4 = pointer3;
These two pointers point to the same object, which means that the memory locations are equal. Because they point to the same object, they also contain the same string of characters. So, from a value perspective, they’re also equal.
Here’s an example. You create a class called Employee
that contains these properties: FirstName
, LastName
, and SocialSecurityNumber
. Next, you create a Salary
class that contains payroll information for an employee. This class has properties MonthlySalary
and Deductions
.
With these two objects in place, you create a map
instance, where each key/value pair contains an Employee
instance for the key and a Salary
instance for the value. To look up an employee, you would make an instance of Employee
and fill in the FirstName
, LastName
, and SocialSecurityNumber
properties. You then retrieve the value based on this key. There are two issues here:
map
to find the key that matches the instance. It’s essential to know whether map
is looking for the exact same instance or one identical to it. When looking for the exact same instance, you need a pointer to the original object, not a new object that you fill in with values.map
is looking for an instance identical to the object you create, the search will fail if the employee changed names (such as during a marriage). In this case, your code needs logic to tell map
to make the match based on the SocialSecurityNumber
, without worrying about the other properties.The previous section provides details on two issues you must resolve when comparing objects. Here’s how to resolve these two issues: If you’re dealing with your own classes, in addition to setting up a container class, you also provide a function that compares two instances of your own class. Your comparison function can determine whether two classes are equal, the first is less than the second, or the first is greater than the second.
The point is that the computer can’t make this decision; you need to choose how you want the data to appear. After you decide how you want them sorted, you’d create a function that determines when one record is less than, equal to, or greater than the other. If you want the list to sort by name, you would make your function look strictly at the names. But if you want your list to sort by Social Security number, you would write your function to compare the Social Security numbers.
The Maps3
example, shown in Listing 6-7, contains a map
class with a comparison function that determines whether two keys are equal.
LISTING 6-7: Containing Instances and Needing Functions That Compare Them
#include <iostream>
#include <map>
using namespace std;
class Emp {
public:
string Nickname;
string SSN;
Emp(string anickname, string asocial) :
Nickname(anickname),
SSN(asocial) {}
Emp() : Nickname(""), SSN("") {}
};
class Salary {
public:
int YearlyInc;
int Taxes;
Salary(int aannual, int adeductions) :
YearlyInc(aannual),
Taxes(adeductions) {}
Salary() : YearlyInc(0), Taxes(0) {}
};
bool operator < (const Emp& first, const Emp& second) {
return first.Nickname < second.Nickname;
}
int main() {
map<Emp, Salary> employees;
Emp emp1("sparky", "123-22-8572");
Salary sal1(135000, 18);
employees[emp1] = sal1;
Emp emp2("buzz", "234-33-5784");
Salary sal2(150000, 23);
employees[emp2] = sal2;
// Now test it out!
Emp emptest("sparky", "");
cout << employees[emptest].YearlyInc << endl;
return 0;
}
When you run this application, you see the YearlyInc
member of the Salary
value, where the key is an Employee
with the name sparky
:
135000
Now notice a couple things about this code. First, to locate the salary for Sparky, you don’t need the Employee
instance for Sparky. Instead, you create an instance of Employee
and set up the Nickname
member without worrying about the SSN
member. Then you retrieve the value by using the bracket notation for map
:
cout << employees[emptest].YearlyInc << endl;
The map
code uses the less-than function to perform this task. The <
function compares only the Nickname
members, not the SSN
member. Notice that this function must return a bool
value and that you precede the <
with the operator
keyword, defining this as an operator function — one that defines an operation between operands. You could change things around a bit by comparing the SSN
members like so:
bool operator < (const Emp& first, const Emp& second) {
return first.SSN < second.SSN;
}
Then you can locate Sparky’s salary based on the SSN:
Employee emptest("", "123-22-8572");
cout << employees[emptest].SSN << endl;
Containers in the Standard Library provide an overview of the container’s content. If you have a container filled with objects ,you normally get an overview of what’s there, but being able to drill down into the details would be nice. You use an iterator to drill down into the container. An iterator works with a container to let you step object by object through the container. The following sections tell you about iterators and how to work with them.
Each container class contains an embedded type called iterator
. You use the fully qualified name to create an iterator instance. For example, if you have a map
that holds integers and strings, as in map<int, string>
, you create an iterator
instance like this:
map<string, int>::iterator loopy
Although loopy
is an instance of iterator
, some serious typedef
ing is going on, and, in fact, loopy
is a pointer to an item stored inside the container. To initialize loopy
to point to the first item in the container, you call the container’s begin()
method, storing the results in loopy
. Then loopy
will point to the first item in the container. You can access the item by dereferencing loopy
; then, when you’re finished, you can move to the next item by incrementing loopy
like this:
loopy++;
You can use this technique in various ways, such as by using the call to reverse_copy()
shown previously in Listing 6-3. You can tell whether you’re finished by checking to see whether loopy
points to the last item in the container. To do this, you call the container’s end()
method and compare loopy
to the end()
value. If it’s equal, you’re done. The following few lines of code perform these steps:
vector<string>::iterator vectorloop = Words.begin();
while (vectorloop != Words.end())
{
cout << *vectorloop << endl;
vectorloop++;
}
You can see the type used for the iterator, in this case called vectorloop
, which is initialized by calling begin()
. vectorloop
is dereferenced to access the data, and is then incremented to get to the next item. The while
loop tests vectorloop
against the results of end()
to determine when the processing is complete. The Iterators
example code, shown in Listing 6-8, shows a more complete example of how to use an iterator
.
LISTING 6-8: Iterating
#include <iostream>
#include <map>
#include <vector>
using namespace std;
int main() {
// Iterating through a map
map<string, int> NumberWords;
NumberWords["ten"] = 10;
NumberWords["twenty"] = 20;
NumberWords["thirty"] = 30;
map<string, int>::iterator loopy = NumberWords.begin();
while (loopy != NumberWords.end()) {
cout << loopy->first << " ";
cout << loopy->second << endl;
loopy++;
}
// Iterating through a vector
vector<string> Words;
Words.push_back("hello");
Words.push_back("there");
Words.push_back("ladies");
Words.push_back("and");
Words.push_back("aliens");
vector<string>::iterator vectorloop = Words.begin();
while (vectorloop != Words.end()) {
cout << *vectorloop << endl;
vectorloop++;
}
return 0;
}
When you compile and run this application, you see the following output:
ten 10
thirty 30
twenty 20
hello
there
ladies
and
aliens
When you create a vector
, it allocates some space for the data you put in it. When the memory fills with data, the vector
resizes itself, adding more space. To perform this task, vector
uses the old memory-shuffle trick where it first allocates a bigger chunk of memory; then it copies the existing data into the beginning of that bigger chunk of memory, and finally it frees the original chunk of memory.
LISTING 6-9: Seeing the Pointer Problem in Action
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> test{1, 2, 3};
vector<int>::iterator i1 = test.begin();
i1++;
cout << &i1 << endl;
test.push_back(4);
vector<int>::iterator i2 = test.begin();
i2++;
cout << &i2 << endl;
}
When you run this example, the code creates test
with space for three items. The code then prints the address of the second item. Adding just one item means that test
has to resize. The code then prints the address for the second item. The outputs won’t match because the pointer to the second item changed during the resizing process.
When you iterate through map
, you get back not just the value of each item nor do you get just the key of each item. Instead, you get back a pair of things — the key and the value together. These objects live inside an instance of a template class called Pair
, which has two properties, first
and second
.
The first member refers to the key in the pair, and the second member refers to the value in the pair. When you iterate through map
, the iterator points to an instance of Pair
, so you can grab the key by looking at first
and the value by looking at second
. Be careful: Pair
is the internal storage bin inside map
. You’re not looking at copies; you’re looking at the actual data in map
. If you change the data, as in this code
while (loopy != NumberWords.end())
{
loopy->second = loopy->second * 2;
loopy++;
}
you change the value stored in map
— not a copy of it.
The sections that follow provide a rundown of containers available in the Standard Library. Each container has a different purpose. In the following sections, you see where you can use each of them.
First things first: set
is not a mathematical set. If you have any background in mathematics, you’ve likely come across the notion of a set. In math, a set doesn’t have an order to it. It’s a group of well-defined distinct objects stored in a collection.
In the Standard Library, set
has an order to it. However, like a math set, set
doesn’t allow duplicates. If you try to put an item in set
that’s already there, set
will ignore your attempt to do so. The Sets
example, shown in Listing 6-10, demonstrates how to use set
.
LISTING 6-10: Using set to Look up Items
#include <iostream>
#include <set>
using namespace std;
class Emp {
public:
string Nickname;
string SSN;
Emp(string anickname, string asocial) :
Nickname(anickname),
SSN(asocial) {}
Emp() : Nickname(""), SSN("") {}
};
bool operator < (const Emp& first, const Emp& second) {
return first.SSN < second.SSN;
}
ostream& operator << (ostream &out, const Emp &emp) {
cout << "(" << emp.Nickname;
cout << "," << emp.SSN;
cout << ")";
return out;
}
int main() {
set<Emp> employees;
Emp emp1("sparky", "123-22-8572");
employees.insert(emp1);
Emp emp2("buzz", "234-33-5784");
employees.insert(emp2);
Emp emp3("albert", "123-22-8572");
employees.insert(emp3);
Emp emp4("sputz", "199-19-0000");
employees.insert(emp4);
// List the items
set<Emp>::iterator iter = employees.begin();
while (iter != employees.end())
{
cout << *iter << endl;
iter++;
}
// Find an item
cout << "Finding…" << endl;
Emp findemp("", "123-22-8572");
iter = employees.find(findemp);
cout << *iter << endl;
return 0;
}
When you compile and run this example, you see the following output:
(sparky,123-22-8572)
(sputz,199-19-0000)
(buzz,234-33-5784)
Finding…
(sparky,123-22-8572)
Listing 6-10 includes an Employee
class along with a <
operator that compares the SSN
member of two Employee
instances. This comparison results in two things:
set
are in Social Security number order. This isn’t true with all containers, but it’s the way a set
works.set
ignores any attempt to add two employees with matching SSN
values (even if other properties differ).You can see in this listing that the code tries to add two employees with the same SSN
values:
Employee emp1("sparky", "123-22-8572");
employees.insert(emp1);
and
Employee emp3("albert", "123-22-8572");
employees.insert(emp3);
Later, when the code prints all the items in set
, you see only the one for "sparky"
, not the one for "albert"
. set
ignored the second employee.
Finding an item in set
is interesting. You create an instance of Employee
and fill in only the SSN
value, because that’s the only property that the <
function looks at. Call find()
to perform the search. The find()
function returns an iterator because the iterator
type is really a typedef
for a pointer to an item inside set
. To access the item, you dereference the pointer.
ostream& operator << (ostream &out, const Employee &emp) {
The first parameter represents cout
, and the second is the output value. Inside this function, you write to cout
the individual members of the Employee
. It also helps to know that you can perform map comparisons as needed using the same technique found in the “Understanding the default <
function” sidebar. This same technique works with a class, but it requires more coding to implement.
When you work with sets, you commonly do the following:
When you #include <set>
, you automatically get a couple of handy functions for finding the union and intersection of some sets. The Sets2
example, shown in Listing 6-11, demonstrates how you can find the intersection and union of two sets.
LISTING 6-11: Finding an Intersection and a Union Is Easy!
#include <iostream>
#include <set>
#include <algorithm>
using namespace std;
void DumpClass(set<string> *myset) {
set<string>::iterator iter = myset->begin();
while (iter != myset->end())
{
cout << *iter << endl;
iter++;
}
}
int main() {
set<string> English;
English.insert("Zeus");
English.insert("Magellan");
English.insert("Vulcan");
English.insert("Ulysses");
English.insert("Columbus");
set<string> History;
History.insert("Vulcan");
History.insert("Ulysses");
History.insert("Ra");
History.insert("Odin");
set<string> Intersect;
insert_iterator<set<string> >
IntersectIterate(Intersect, Intersect.begin());
set_intersection(English.begin(), English.end(),
History.begin(), History.end(), IntersectIterate);
cout << "===Intersection===" << endl;
DumpClass(&Intersect);
set<string> Union;
insert_iterator<set<string> >
UnionIterate(Union, Union.begin());
set_union(English.begin(), English.end(),
History.begin(), History.end(), UnionIterate);
cout << endl << "===Union===" << endl;
DumpClass(&Union);
return 0;
}
When you run the code in Listing 6-11, you see this output:
===Intersection===
Ulysses
Vulcan
===Union===
Columbus
Magellan
Odin
Ra
Ulysses
Vulcan
Zeus
But as you can see, something a little bizarre is in the code. Specifically, this part isn’t exactly simple:
insert_iterator<set<string> >
IntersectIterate(Intersect, Intersect.begin());
This code is used in the call to set_intersection()
. It’s a variable declaration. The first line is the type of the variable, a template called insert_iterator
. The template parameter is the type of set
, in this case set<string>
.
The next line is the instance name, IntersectIterate
, and the constructor requires two things: the set
that will hold the intersection (called Intersect
) and an iterator pointing to the beginning of the set
, which is Intersect.begin()
.
The variable that these two lines create is an iterator, which is a helper object that another function can use to insert multiple items into a list. In this case, the function is set_intersection()
. The set_intersection()
function doesn’t take the sets as input; instead, it takes the beginning and ending iterators of the two sets, along with the IntersectIterate
iterator declared earlier. You can see in Listing 6-11 that those are the five items passed to the set_intersection()
function. After calling set_intersection()
, the Intersect
object contains the intersection of the two sets. set_union()
works precisely the same way as set_intersection()
, except it figures out the union of the two sets, not the intersection.
set_intersection(English.begin(), English.end(),
History.begin(), History.end(),
inserter(Intersect, Intersect.begin()));
The third line simply calls inserter()
, which creates an instance of insert_iterator
for you. Then you can do the same for set_union()
:
set_union(English.begin(), English.end(),
History.begin(), History.end(),
inserter(Union, Union.begin()));
A list
is a simple container similar to an array, except you can’t access the members of list
by using a bracket notation as you can in vector
or with an array. You don’t use list
when you need to access only one item in the list; you use it when you plan to traverse through the list, item by item.
To add items to a list, use the list’s push_front()
method or its push_back()
method. The push_front()
function inserts the item in the beginning of the list, in front of all the others that are presently in the list. If you use push_front()
several times in a row, the items will be in the reverse order from which you put them in. The push_back()
function adds the item to the end of the list. So if you put items in a list by using push_back()
, their order will be the same as the order in which you added them. Using insert()
and splice()
enables you to place items in other locations in the list. You use an iterator to find the location you want and then splice the new item at that location.
The Lists
example, shown in Listing 6-12, demonstrates lists by using a duck metaphor (as in, getting all your ducks in a row). This example creates a list, adds ducks, and then reverses it. Next, the code creates a second list and splices its members into the first list.
LISTING 6-12: Handling Items in a List Template
#include <iostream>
#include <list>
using namespace std;
class Duck {
public:
string name;
int weight;
int length;
};
ostream& operator << (ostream &out, const Duck &duck) {
cout << "(" << duck.name;
cout << "," << duck.weight;
cout << "," << duck.length;
cout << ")";
return out;
}
void Dump(list<Duck> *mylist) {
list<Duck>::iterator iter = mylist->begin();
while (iter != mylist->end())
{
cout << *iter << endl;
iter++;
}
}
list<Duck>::iterator Move(list<Duck> *mylist, int pos) {
list<Duck>::iterator res = mylist->begin();
for (int loop = 1; loop <= pos; loop++)
{
res++;
}
return res;
}
bool operator < (const Duck& first, const Duck& second) {
return first.name < second.name;
}
int main() {
list<Duck> Inarow;
// Push some at the beginning
Duck d1 = {"Jim", 20, 15}; // Braces notation!
Inarow.push_front(d1);
Duck d2 = {"Sally", 15, 12};
Inarow.push_front(d2);
// Push some at the end
Duck d3 = {"Betty", 18, 25};
Inarow.push_front(d3);
Duck d4 = {"Arnold", 19, 26};
Inarow.push_front(d4);
// Display the ducks
cout << "===Ducks===" << endl;
Dump(&Inarow);
// Reverse
Inarow.reverse();
cout << "\n==Reversed==" << endl;
Dump(&Inarow);
// Create the second list.
list<Duck> extras;
Duck d5 = {"Grumpy", 8, 8};
extras.push_back(d5);
Duck d6 = {"Sleepy", 8, 8};
extras.push_back(d6);
// Display the extras list.
cout << "\n===Extras===" << endl;
Dump(&extras);
// Determine the positions.
list<Duck>::iterator first = Move(&extras, 0);
list<Duck>::iterator last = Move(&extras, 2);
list<Duck>::iterator into = Move(&Inarow, 2);
// Perform the splicing.
Inarow.splice(into, extras, first, last);
cout << "\n==Extras After Splice==" << endl;
Dump(&extras);
cout << "\n==Inarow After Splice==" << endl;
Dump(&Inarow);
// Sort the list.
Inarow.sort();
cout << "\n===Sorted===" << endl;
Dump(&Inarow);
return 0;
}
Move()
moves to a position in the list. This function may seem counterproductive because the list
template doesn’t allow random access. But you need three iterators to perform the splice: two to target the start and end position of the second list (the source list) and one to target the position in the first list used to hold the spliced members. Move()
locates the target position.
To use sort()
, you must provide a <
operator function, as described in earlier examples. Here’s the application output:
===Ducks===
(Arnold,19,26)
(Betty,18,25)
(Sally,15,12)
(Jim,20,15)
==Reversed==
(Jim,20,15)
(Sally,15,12)
(Betty,18,25)
(Arnold,19,26)
===Extras===
(Grumpy,8,8)
(Sleepy,8,8)
==Extras After Splice==
==Inarow After Splice==
(Jim,20,15)
(Sally,15,12)
(Grumpy,8,8)
(Sleepy,8,8)
(Betty,18,25)
(Arnold,19,26)
===Sorted===
(Arnold,19,26)
(Betty,18,25)
(Grumpy,8,8)
(Jim,20,15)
(Sally,15,12)
(Sleepy,8,8)
You can see the elements that were inside the two lists before and after the splice; the ducks moved from one list to another.
A double-ended queue, deque
(pronounced “deck”), container is a sequential list of items like vector
and list
. Like vector
s and unlike list
s, deque
s allow random access using bracket notation. Unlike vector
, deque
lets you push (insert) items at the beginning or end and pop (remove) items off the beginning or end. To create a deque
that holds integers, do something like this:
deque<int> mydek;
mydek.push_front(10);
mydek.push_front(20);
mydek.push_back(30);
mydek.push_back(40);
Then you can loop through the deque
, accessing its members with a bracket, as if it’s an array:
int loop;
for (loop = 0; loop < mydek.size(); loop++) {
cout << mydek[loop] << endl;
}
You can also grab items off the front or back of the deque
. Here’s an example from the front:
while (mydek.size() > 0) {
cout << mydek.front() << endl;
mydek.pop_front();
}
Two functions show up here, front()
and pop_front()
. The front()
function returns a reference to the item at the front of the deque
. The pop_front()
function removes the item that’s at the front of the deque
.
Two common programming data structures are in the Standard Library:
To use the Standard Library to make a stack, you can use a deque
, a list
, or a vector
as the underlying storage bin. Then you declare the stack, as in the following example:
stack<int, vector<int> > MyStack;
Or you can optionally use the default, which is deque
:
stack<int> MyStack;
For a queue, you can’t use vector
because vectors
don’t include operations for dealing with the front of an item list. So, you can use either deque
or list
. Here’s a line of code that uses list
:
queue<int, list<int> > MyQueue;
Or here’s a line of code that uses deque
by default:
queue<int> MyQueue;
You normally perform three operations with a stack and a queue:
To peek at the front of a queue, you call the front()
method. For a stack, you call the top()
method. For pushing and popping, the queue
and stack
each include a push()
function and a pop()
function. The StackAndQueue
example, shown in Listing 6-13, demonstrates both a stack and a queue.
LISTING 6-13: Creating a Stack and a Queue
#include <iostream>
#include <stack>
#include <queue>
using namespace std;
void StackDemo() {
cout << "===Stack Demo===" << endl;
stack<int, vector<int> > MyStack;
MyStack.push(5);
MyStack.push(10);
MyStack.push(15);
cout << MyStack.top() << endl;
MyStack.pop();
cout << MyStack.top() << endl;
MyStack.pop();
MyStack.push(40);
cout << MyStack.top() << endl;
MyStack.pop();
}
void QueueDemo() {
cout << "===Queue Demo===" << endl;
queue<int> MyQueue;
MyQueue.push(5);
MyQueue.push(10);
MyQueue.push(15);
cout << MyQueue.front() << endl;
MyQueue.pop();
cout << MyQueue.front() << endl;
MyQueue.pop();
MyQueue.push(40);
cout << MyQueue.front() << endl;
MyQueue.pop();
}
int main() {
StackDemo();
QueueDemo();
return 0;
}
===Stack Demo===
15
10
40
===Queue Demo===
5
10
15
Structures are easy to copy when using well-designed class libraries — meaning that each container class contains both a copy constructor and an equal operator. To copy a container, you either set one equal to the other or pass the first container into the constructor of the second. The CopyContainer
example shown in Listing 6-14 demonstrates how to perform this task.
LISTING 6-14: Copying Containers Couldn’t Be Easier
#include <iostream>
#include <map>
using namespace std;
class Tasty {
public:
string Dessert;
};
bool operator < (const Tasty & One, const Tasty & Two) {
return One.Dessert < Two.Dessert;
}
class Nutrition {
public:
int VitaminC;
int Potassium;
};
int main() {
map<Tasty, Nutrition> ItsGoodForMe;
Tasty ap = {"Apple Pie"}; // Braces notation!
Nutrition apn = {7249, 9722};
Tasty ic = {"Ice Cream"};
Nutrition icn = {2459, 19754};
Tasty cc = {"Chocolate Cake"};
Nutrition ccn = {9653, 24905};
Tasty ms = {"Milk Shake"};
Nutrition msn = {46022, 5425};
ItsGoodForMe[ap] = apn;
ItsGoodForMe[ic] = icn;
ItsGoodForMe[cc] = ccn;
ItsGoodForMe[ms] = msn;
map<Tasty,Nutrition> Duplicate1 = ItsGoodForMe;
map<Tasty,Nutrition> Duplicate2(ItsGoodForMe);
ItsGoodForMe[ap].Potassium = 20;
Duplicate1[ap].Potassium =40;
cout << ItsGoodForMe[ap].Potassium << endl;
cout << Duplicate1[ap].Potassium << endl;
cout << Duplicate2[ap].Potassium << endl;
return 0;
}
You can see that Listing 11-14 contains two classes, Tasty
and Nutrition
. A map
called ItsGoodForMe
associates Tasty
instances with Nutrition
instances. The code copies map
twice, using both an equals sign and a copy constructor:
map<Tasty,Nutrition> Duplicate1 = ItsGoodForMe;
map<Tasty,Nutrition> Duplicate2(ItsGoodForMe);
The code changes one of the elements in the original map
to see what happens and prints that element, as well as the corresponding element in the two copies (one of which is also changed). Here’s the output:
20
40
9722
The output implies that the map
s each have their own copies of the instances — that there’s no sharing of instances between the map
s.
Sometimes you don’t know the array size you need until runtime. The default arrays provided with C++ rely on static sizes. In other words, you need to know what size array you need at the time you write the code. Unfortunately, the real world is dynamic — it changes. The earlier sections of this chapter discuss a number of array alternatives, such as stacks, queues, and deques. However, these solutions all require that you use a library. They also tend to increase the memory requirements of your application and slow it down as well. You have another alternative in the form of dynamic arrays. The following sections describe dynamic arrays and show how to use them. You need a minimum of C++ 11 to use these examples.
A dynamic array relies on the heap, the common area of memory that your application allocates for use by your application’s functions. (See the “Heaping and Stacking the Variables” section of Book 1 Chapter 8 for more details.) You create a pointer to a variable of the correct type and then allocate memory for the resulting array. The functionality for performing this task is found in the new
header file, so you need to include it as part of your application. The DynamicArray
example, shown in Listing 6-15, demonstrates the use of a dynamic array.
LISTING 6-15: Creating and Using Dynamic Arrays
#include <iostream>
#include <new>
using namespace std;
int main() {
int HowMany;
int* DynArray;
cout << "How many numbers would you like?" << endl;
cin >> HowMany;
DynArray = new (nothrow) int[HowMany];
if (DynArray == nullptr)
cout << "Error: Could not allocate memory!";
else {
for(int i = 0; i < HowMany; i++)
DynArray[i] = i;
cout << "Displaying entries:" << endl;
for (int i = 0; i < HowMany; i++)
cout << DynArray[i] << endl;
delete[] DynArray;
}
return 0;
}
The example begins by creating variables to hold the number of array elements and the array itself, which is a pointer to an array of int
elements. The application asks you how many array elements to create. It then uses the new
operator to create the dynamic array, DynArray
. Notice the technique used to do this. The new
operator is followed by (nothrow)
. This tells the application that if there isn’t enough memory to create the array, it should return a nullptr
value, which is simply a pointer that doesn’t point to anything.
You work with a dynamic array just as you do any other array. The example shows how to fill the array with data and to display the data onscreen. The array has the same capabilities, advantages, and disadvantages of any other array. However, when you get done using the array, you need to use the delete[]
operator to delete the dynamic array and free the memory it uses for some other purpose. The output from this example looks like this if you request four array elements:
How many numbers would you like?
4
Displaying entries:
0
1
2
3
Creating data that has a particular order is appealing because it’s a) easier to search and b) it makes certain tasks, such as removing old elements, easier. However, creating an ordered set of information is also problematic because you need to spend time keeping it in order. Newer versions of C++ provide access to an unordered set that is both searchable and easy to maintain. It has the advantage of adding the data in any order in which it comes. Minimal overhead is associated with trying to keep the data in a particular order. The following sections provide an overview of using unordered data. You need a minimum of C++ 11 to use these examples.
Like the other containers discussed in this chapter, an unordered_set
provides a particular method for storing data in a manner that makes it easy to access later. In this case, you have access to functions that insert()
and erase()
elements from the container. A special function, emplace()
enables you to add new elements only if the element doesn’t exist. Otherwise, the unordered set will allow as many duplicates as you want (and you can easily count them using count()
). You can also use the find()
function to track down elements that you want. Special functions tell you when you’re at the beginning or end of the set.
The easiest way to see how an unordered set works is to create one. The UnorderedSet
example, shown in Listing 6-16, demonstrates how to use the various unordered_set
features to maintain a listing of colors.
LISTING 6-16: Creating and Using Dynamic Arrays
#include <iostream>
#include <unordered_set>
using namespace std;
int main() {
unordered_set<string> Colors;
Colors.insert("Red");
Colors.insert("Green");
Colors.insert("Blue");
if(Colors.find("Red")!= Colors.end())
cout << "Found Red!" << endl;
auto ReturnValue = Colors.emplace("Red");
if(!ReturnValue.second)
cout << "Red is Already in Set!" << endl;
cout << "There are " << Colors.count("Red")
<< " Red entries." << endl;
ReturnValue = Colors.emplace("Orange");
if(!ReturnValue.second)
cout << "Orange is Already in Set!" << endl;
else
cout << "Orange Added to Set!" << endl;
Colors.erase("Red");
if(Colors.find("Red")!= Colors.end())
cout << "Found Red!" << endl;
else
cout << "Red Missing!" << endl;
return 0;
}
The example begins by creating a new unordered_set
, Colors
. Notice that this is a template, so you need to provide a type for the information that the set will hold. The code uses the insert()
function to add three colors to the set.
The find()
function enables you to look for a particular value in the set. When the value is missing, the find()
function returns end()
, which means that the current position within the set is at the end.
This example uses the auto
data type. ReturnValue
is used to detect when a value that you want to add to the set using emplace()
already exists. If the value already exists, unordered_set
refuses to add it when you call emplace()
. On the other hand, if you call insert()
, unordered_set
will add duplicate entries.
To remove entries from a set, you call erase()
with the value you want to remove. In this case, the example removes the color Red
. It then searches for Red
using find()
. As you might expect, Red
isn’t found this time. The output from this example is as follows:
Found Red!
Red is Already in Set!
There are 1 Red entries.
Orange Added to Set!
Red Missing!
C++ now offers support for ranges, which you can read about at https://en.cppreference.com/w/cpp/ranges
. A range is the set of objects between a beginning point and an ending point. The concept of ranges depends on iterators. A view is an iteration that manages data in some manner, and a range acts on this iteration. The example in this section requires C++ 20. As mentioned elsewhere in the book, such as Book 1 Chapter 5, if your compiler doesn’t offer C++ 20 support, you can use Wandbox (https://wandbox.org/
).
The Ranges
example, shown in Listing 6-17, gives you a starting point for working with both ranges and views in C++ 20. In this example, the code creates a vector
, MyList
, fills it with data, and then uses the ranges::size()
method to determine the size MyList
. The code then creates a view that filters MyList
and places the result in Filtered
. A for
loop prints the result.
LISTING 6-17: Working with Ranges and Views
#include <iostream>
#include <vector>
#include <ranges>
using namespace std;
int main() {
vector<int> MyList {9, 2, 1, 6, 3, 8, 4};
cout << "There are " << ranges::size(MyList) <<
" items in MyList" << endl;
auto Filtered = MyList | views::filter([](int n){
return n % 3 == 0; });
cout << "Items divisible by 3: " << endl;
for (int i : Filtered)
cout << i << endl;
return 0;
}
Filtered
is a range adapter, which is an iterable range. To create this variable, you specify the list you want to use, such as MyList
, a pipe symbol (|
), and the view or views you want to create. This example uses views::filter()
. When you want to create multiple views, you separate them with addition pipes. You can also find views that will transform your data, drop certain elements, split ranges, join ranges, and so on. Whatever the range adapter creates appears in Filtered
. As the code shows, you iterate over the view using a standard for
loop.