Class Templates
Steven J. Zeil
Just as function templates allow us to write general patterns for functions, from which the compiler then generates multiple instances, so class templates allow us to write general patterns for classes.
Class templates are especially useful for “containers”, data structures that serve mainly as collections of other, smaller data types. In our spell checker, for example, we have used two different kinds of sets and two different kinds of ordered sequence. The code for the two sets was essentially the same – only the data types of the contained data varied. The same is true if we compare the code for the two kinds of sequences.
Now, we certainly can get these different versions of the same container class by copying a file and editing it. But, as you’ve seen in prior lecture, this approach can be tedious and error-prone. It also creates project management nightmares when a bug is fixed in one copy of an often-replicated container, and we must then find and fix all the copies.
1 Class Templates
-
A template is a “pattern” for a class or function in which certain type names and constants are left as general template parameters.
-
These template parameters are specified in a template header at the start of the class declaration. This template header consists of the keyword “
template
” followed by the list of replaceable names inside<...>
. -
We instantiate a class template when we are ready to use it by supplying the parameter value:
classTemplateName<myFavoriteType> myVariable;
-
the resulting class is an instance of the template.
You’ve probably used some class templates before. In CS250/333 you would have made good use of std::vector
as a kind of expandable array:
vector<string> names;
// Read names from a file.
ifstream input ("names.txt");
string oneName;
while (input >> oneName) {
names.push_back (oneName);
}
int numberOfNamesRead = names.size();
But you may or may not have written your own class templates yet.
2 Developing and Using Class Templates
2.1 Templates as a Pattern for Data
Here’s some basic code declaring a typical sequence of string
s, together with a function that inserts a data value into the sequence in a specific position.
struct Sequence {
string* data;
int numStrings;
};
void add (Sequence& toSequence, string newValue, int position)
{
for (int i = toSequence.numStrings-1; i >= position; --i)
toSequence.data[i+1] = toSequence.data[i];
toSequence.data[position] = newValue;
++toSequence.numStrings;
}
Notice that the code to insert something into this sequence would be the same if it were a sequence of int
instead of string
, or a sequence of double
, or a sequence of PayrollRecord
s. That’s a sign that this structure might be a good candidate for being turned into a template.
2.1.1 Adding a Template Header
We start the conversion of this into a template class1 by adding a template header to introduce a new “placeholder” name, Data
, to represent the data stored in the sequence.
template <class Data>
struct Sequence {
Data* data;
int numStrings;
};
void add (Sequence& toSequence, string newValue, int position)
{
for (int i = toSequence.numStrings-1; i >= position; --i)
toSequence.data[i+1] = toSequence.data[i];
toSequence.data[position] = newValue;
++toSequence.numStrings;
}
At this point, the name “Sequence
” no longer defines a class/struct, but a pattern for an infinite number of possible classes.
That’s all we need to do to turn Sequence
into a class template. Now we can declare sequences of any kinds of data that we want.
Of course, that may not be very useful if the only functions we have for manipulating sequences assume, like add here, that their sequences only contain string data. So we really need to turn add into a template function …
template <class Data>
struct Sequence {
Data* data;
int numStrings;
};
template <class Data>
void add (Sequence<Data>& toSequence, Data newValue, int position)
{
for (int i = toSequence.numStrings-1; i >= position; --i)
toSequence.data[i+1] = toSequence.data[i];
toSequence.data[position] = newValue;
++toSequence.numStrings;
}
Notice that, everywhere we used to just say Sequence
, now we must instantiate the class template by saying Sequence<...>
.
2.2 Instantiating Class Templates
Instantiating class templates is a little different from instantiating function templates. When we have a function template like:
template <class T>
inline const T& min(const T& a, const T& b) {
return b < a ? b : a;
}
we instantiate it simply by using it:
int i, j, k;
double x, y, z;
⋮
k = min(i, j); // instantiated with T => int
z = min(x, y); // instantiated with T => double
and the compiler would infer the appropriate replacements for the template parameters by examining the data types of the actual parameters.
But with a class template, we supply the replacements ourselves when we use the class as a type name:
Sequence<PayrollRecord> peopleGettingRaises;
2.3 Templates with Function Members
The example we’ve looked at was pretty simple. In particular, the class we used had no member functions.
Now, let’s start over, this time with some member functions.
struct Sequence {
string* data;
int numStrings;
Sequence (int maxSize);
void add (string newValue, int position);
};
Sequence::Sequence (int maxSize)
: numStrings(0)
{
data = new string[maxSize];
}
void Sequence::add (string newValue, int position)
{
for (int i = numStrings-1; i >= position; --i)
data[i+1] = data[i];
data[position] = newValue;
++numStrings;
}
In this case, I’ve added a constructor that initializes the data fields of the sequence and turned the add function into a member function.
Now, we start by “templatizing” the class declaration and the sequentialInsert
function just as we did with sequentialSearch
earlier …
2.3.1 Add the Header and Update the Date Type References
template <class Data>
struct Sequence {
Data* data;
int numStrings;
Sequence (int maxSize);
void add (Data newValue, int position);
};
Sequence::Sequence (int maxSize)
: numStrings(0)
{
data = new string[maxSize];
}
void Sequence::add (string newValue, int position)
{
for (int i = numStrings-1; i >= position; --i)
data[i+1] = data[i];
data[position] = newValue;
++numStrings;
}
But what to do about the body of the member functions?
2.3.2 Member Functions Become Member Function Templates
Member functions of a template class are, implicitly, templates and so when we supply their bodies, we have to add a template header to indicate what names we are using for the placeholders.
template <class Data>
struct Sequence {
Data* data;
int numStrings;
Sequence (int maxSize);
void add (Data newValue, int position);
};
template <class Data>
Sequence<Data>::Sequence (int maxSize)
: numStrings(0)
{
data = new string[maxSize];
}
template <class Data>
void Sequence<Data>::add (Data newValue, int position)
{
for (int i = numStrings-1; i >= position; --i)
data[i+1] = data[i];
data[position] = newValue;
++numStrings;
}
So we convert the member function by writing it as a function template. (In fact, the add
member function body winds up looking a lot like the earlier example of the add
non-member function template.)
2.4 Tips for Implementing Templates
For more complicated classes, I recommend the following approach to implementing class templates:
-
Start with a working non-template class in which you have used the template parameter names but employed
typedef
’s to supply replacements for the parameter names.(Strictly speaking, this step isn’t necessary. But compilers are able to do more checking for you this way, and often will issue much clearer error messages.)
-
Combine everything from the class’s
.h
and.cpp
files into the.h
file. -
Get rid of the phony typedefs, and replace by a template header at the start of the class, declaring the template parameters. Add a similar template header to each member definition.
-
Change all uses of the class name, except for constructor and destructor names, to
theClassName
<templateParams
>. -
Check all closely related, nested, and friend classes to see if they need to be converted to templates as well.
3 Example – Matrix
Arrays in C++ may seem like familiar old friends to you. Certainly, you should be comfortable with singly-dimensioned arrays. But many problems require a two-dimensional array.
This can be done working directly with C++ arrays (by having an array of arrays) but initializing and cleaning up after such structures can be awkward and error-prone. As with many data structures, it may be worth putting in the effort to develop a class that provides this capability so that, having put in the effort once to get it working, we can use it with confidence many times in the future.
Here is the header file for a matrix-of-floating-point-numbers class.2
#ifndef MATRIX_H
#define MATRIX_H
#include <algorithm>
#include <cassert>
class Matrix
//
// Provides a "2-dimensional" rectangular
// array-like structure for indexing using
// a pair of indices.
{
public:
Matrix();
Matrix (unsigned theLength1, unsigned theLength2);
Matrix (const Matrix&);
~Matrix();
const Matrix& operator= (const Matrix&);
// Indexing into the matrix: What we would like to do is allow
// myMatrix[i,j]. But C++ allows operator[] only to take a single
// parameter. But operator() can take whatever parameters we like.
// So we can write myMatrix(i,j).
double& operator() (int i1, int i2);
const double& operator() (int i1, int i2) const;
unsigned length1() const;
unsigned length2() const;
bool operator== (const Matrix&) const;
private:
double* data;
unsigned _length1;
unsigned _length2;
};
#endif
The class itself is pretty straightforward except for the declaration of an operator()
function taking two integer parameters. It may seem a bit strange to think of ()
as an operator, but C++ treats it that way. So we can use this class like this:
Matrix m(4,3);
for (int i = 0; i < 4; ++i)
{
m(i,0) = (double)i;
for (int j = 1; j < 3; ++j)
m(i,j) = m(i,j-1) + j;
}
The expressions m(i,0)
, m(i,j)
, etc., are actually invoking that odd-looking operator. It’s very much like working with an array, except for the use of ()
rather than []
.
Here is the implementation of the matrix class:
#include "matrix.h"
Matrix::Matrix()
: data(0), _length1(0), _length2(0)
{}
Matrix::Matrix(unsigned theLength1, unsigned theLength2)
: _length1(theLength1), _length2(theLength2)
{
data = new double[theLength1*theLength2];
}
Matrix::Matrix(const Matrix& m)
: _length1(m._length1), _length2(m._length2)
{
data = new double[theLength1*theLength2];
copy (m.data, m.data+theLength1*theLength2, data);
}
Matrix::~Matrix()
{
delete [] data;
}
const Matrix& Matrix::operator= (const Matrix& m)
{
if (this != &m)
{
if (_length1*_length2 < m._length1*m._length2)
{
delete [] data;
data = new double[m._length1*m._length2];
}
_length1 = m._length1;
_length2 = m._length2;
copy (m.data, m.data+_length1*_length2, data);
}
return *this;
}
// Indexing into the matrix: What we would like to do is allow
// myMatrix[i,j]. But C++ allows operator[] only to take a single
// parameter. But operator() can take whatever parameters we like.
// So we can write myMatrix(i,j).
double& Matrix::operator() (int i1, int i2)
{
assert ((i1 >= 0) && (i1 < _length1));
assert ((i2 >= 0) && (i2 < _length2));
return data[i1 + _length1*i2];
}
const double& Matrix::operator() (int i1, int i2) const
{
assert ((i1 >= 0) && (i1 < _length1));
assert ((i2 >= 0) && (i2 < _length2));
return data[i1 + _length1*i2];
}
inline
unsigned Matrix::length1() const
{
return _length1;
}
inline
unsigned Matrix::length2() const
{
return _length2;
}
bool Matrix::operator== (const Matrix& m) const
{
return (_length1 == m._length1)
&& (_length2 == m._length2)
&& equal (data, data+_length1*_length2, m.data);
}
The implementation file for this class is pretty straightforward once you grasp the key idea that a two-dimensional array can be mapped onto a one-dimensional array be taking advantage of the formula $i + j*n_{i}$, which maps each possible (i,j) pair onto a unique integer position, if $n_{i}$ is the number of values in each “row” (i.e., the number of possible values of i
). For example, the matrix m
declared as
Matrix m(4,3);
would store element (0,0) in position 0, (1,0) in position 1, (0,1) in position 4, (1,1) in position 5, etc.
So we would visualize this matrix as
but implement it as
Now, how to convert this to a more general matrix-of-anything template? To do that, we would take the matrix-of-double code and
-
Start with a working non-template class using template parameter names but employing typedefs to supply replacements for the parameter names.
-
Combine everything from the class’s .h and .cpp files into the .h file.
-
Get rid of the phony typedefs, and replace by a template header at the start of the class, declaring the template parameters. Add a similar template header to each member definition.
-
Change all uses of the class name, except for constructor and destructor names, to the
ClassName
<templateParams
>’. -
Check all closely related, nested, and friend classes to see if they need to be converted to templates as well.
We’ll take it in steps.
3.1 Start with a working class…
#ifndef MATRIX_H
#define MATRIX_H
#include <algorithm>
#include <cassert>
typedef double Element;
class Matrix
//
// Provides a "2-dimensional" rectagular
// array-like structure for indexing using
// a pair of indices.
{
public:
Matrix();
Matrix (unsigned theLength1, unsigned theLength2);
Matrix (const Matrix&);
~Matrix();
const Matrix& operator= (const Matrix&);
// Indexing into the matrix: What we would like to do is allow
// myMatrix[i,j]. But C++ allows operator[] only to take a single
// parameter. But operator() can take whatever parameters we like.
// So we can write myMatrix(i,j).
Element& operator() (int i1, int i2);
const Element& operator() (int i1, int i2) const;
unsigned length1() const;
unsigned length2() const;
bool operator== (const Matrix&) const;
private:
Element* data;
unsigned _length1;
unsigned _length2;
};
#endif
#include "matrix.h"
Matrix::Matrix()
: data(0), _length1(0), _length2(0)
{}
Matrix::Matrix(unsigned theLength1, unsigned theLength2)
: _length1(theLength1), _length2(theLength2)
{
data = new Element[theLength1*theLength2];
}
Matrix::Matrix(const Matrix& m)
: _length1(m._length1), _length2(m._length2)
{
data = new Element[theLength1*theLength2];
copy (m.data, m.data+theLength1*theLength2, data);
}
Matrix::~Matrix()
{
delete [] data;
}
const Matrix& Matrix::operator= (const Matrix& m)
{
if (this != &m)
{
if (_length1*_length2 < m._length1*m._length2)
{
delete [] data;
data = new Element[m._length1*m._length2];
}
_length1 = m._length1;
_length2 = m._length2;
copy (m.data, m.data+_length1*_length2, data);
}
return *this;
}
// Indexing into the matrix: What we would like to do is allow
// myMatrix[i,j]. But C++ allows operator[] only to take a single
// parameter. But operator() can take whatever parameters we like.
// So we can write myMatrix(i,j).
Element& Matrix::operator() (int i1, int i2)
{
assert ((i1 >= 0) && (i1 < _length1));
assert ((i2 >= 0) && (i2 < _length2));
return data[i1 + _length1*i2];
}
const Element& Matrix::operator() (int i1, int i2) const
{
assert ((i1 >= 0) && (i1 < _length1));
assert ((i2 >= 0) && (i2 < _length2));
return data[i1 + _length1*i2];
}
inline
unsigned Matrix::length1() const
{
return _length1;
}
inline
unsigned Matrix::length2() const
{
return _length2;
}
bool Matrix::operator== (const Matrix& m) const
{
return (_length1 == m._length1)
&& (_length2 == m._length2)
&& equal (data, data+_length1*_length2, m.data);
}
We expect that Matrix
is already working. We start by changing all the instances of double
to a name we can eventually use as a template parameter.
3.2 Combine into .h File
#ifndef MATRIX_H
#define MATRIX_H
#include <algorithm>
#include <cassert>
typedef double Element;
class Matrix
//
// Provides a "2-dimensional" rectagular
// array-like structure for indexing using
// a pair of indices.
{
public:
Matrix();
Matrix (unsigned theLength1, unsigned theLength2);
Matrix (const Matrix&);
~Matrix();
const Matrix& operator= (const Matrix&);
// Indexing into the matrix: What we would like to do is allow
// myMatrix[i,j]. But C++ allows operator[] only to take a single
// parameter. But operator() can take whatever parameters we like.
// So we can write myMatrix(i,j).
Element& operator() (int i1, int i2);
const Element& operator() (int i1, int i2) const;
unsigned length1() const;
unsigned length2() const;
bool operator== (const Matrix&) const;
private:
Element* data;
unsigned _length1;
unsigned _length2;
};
Matrix::Matrix()
: data(0), _length1(0), _length2(0)
{}
Matrix::Matrix(unsigned theLength1, unsigned theLength2)
: _length1(theLength1), _length2(theLength2)
{
data = new Element[theLength1*theLength2];
}
Matrix::Matrix(const Matrix& m)
: _length1(m._length1), _length2(m._length2)
{
data = new Element[theLength1*theLength2];
copy (m.data, m.data+theLength1*theLength2, data);
}
Matrix::~Matrix()
{
delete [] data;
}
const Matrix& Matrix::operator= (const Matrix& m)
{
if (this != &m)
{
if (_length1*_length2 < m._length1*m._length2)
{
delete [] data;
data = new Element[m._length1*m._length2];
}
_length1 = m._length1;
_length2 = m._length2;
copy (m.data, m.data+_length1*_length2, data);
}
return *this;
}
// Indexing into the matrix: What we would like to do is allow
// myMatrix[i,j]. But C++ allows operator[] only to take a single
// parameter. But operator() can take whatever parameters we like.
// So we can write myMatrix(i,j).
Element& Matrix::operator() (int i1, int i2)
{
assert ((i1 >= 0) && (i1 < _length1));
assert ((i2 >= 0) && (i2 < _length2));
return data[i1 + _length1*i2];
}
const Element& Matrix::operator() (int i1, int i2) const
{
assert ((i1 >= 0) && (i1 < _length1));
assert ((i2 >= 0) && (i2 < _length2));
return data[i1 + _length1*i2];
}
inline
unsigned Matrix::length1() const
{
return _length1;
}
inline
unsigned Matrix::length2() const
{
return _length2;
}
bool Matrix::operator== (const Matrix& m) const
{
return (_length1 == m._length1)
&& (_length2 == m._length2)
&& equal (data, data+_length1*_length2, m.data);
}
#endif
Easy enough. Don’t forget though, to move the #endif
down to the new bottom.
3.3 Add Template Parameters
#ifndef MATRIX_H
#define MATRIX_H
#include <algorithm>
#include <cassert>
template <class Element>
class Matrix
//
// Provides a "2-dimensional" rectagular
// array-like structure for indexing using
// a pair of indices.
{
public:
Matrix();
Matrix (unsigned theLength1, unsigned theLength2);
Matrix (const Matrix&);
~Matrix();
const Matrix& operator= (const Matrix&);
// Indexing into the matrix: What we would like to do is allow
// myMatrix[i,j]. But C++ allows operator[] only to take a single
// parameter. But operator() can take whatever parameters we like.
// So we can write myMatrix(i,j).
Element& operator() (int i1, int i2);
const Element& operator() (int i1, int i2) const;
unsigned length1() const;
unsigned length2() const;
bool operator== (const Matrix&) const;
private:
Element* data;
unsigned _length1;
unsigned _length2;
};
template <class Element>
Matrix::Matrix()
: data(0), _length1(0), _length2(0)
{}
template <class Element>
Matrix::Matrix(unsigned theLength1, unsigned theLength2)
: _length1(theLength1), _length2(theLength2)
{
data = new Element[theLength1*theLength2];
}
template <class Element>
Matrix::Matrix(const Matrix& m)
: _length1(m._length1), _length2(m._length2)
{
data = new Element[theLength1*theLength2];
copy (m.data, m.data+theLength1*theLength2, data);
}
template <class Element>
Matrix::~Matrix()
{
delete [] data;
}
template <class Element>
const Matrix& Matrix::operator= (const Matrix& m)
{
if (this != &m)
{
if (_length1*_length2 < m._length1*m._length2)
{
delete [] data;
data = new Element[m._length1*m._length2];
}
_length1 = m._length1;
_length2 = m._length2;
copy (m.data, m.data+_length1*_length2, data);
}
return *this;
}
// Indexing into the matrix: What we would like to do is allow
// myMatrix[i,j]. But C++ allows operator[] only to take a single
// parameter. But operator() can take whatever parameters we like.
// So we can write myMatrix(i,j).
template <class Element>
Element& Matrix::operator() (int i1, int i2)
{
assert ((i1 >= 0) && (i1 < _length1));
assert ((i2 >= 0) && (i2 < _length2));
return data[i1 + _length1*i2];
}
template <class Element>
const Element& Matrix::operator() (int i1, int i2) const
{
assert ((i1 >= 0) && (i1 < _length1));
assert ((i2 >= 0) && (i2 < _length2));
return data[i1 + _length1*i2];
}
template <class Element>
unsigned Matrix::length1() const
{
return _length1;
}
template <class Element>
unsigned Matrix::length2() const
{
return _length2;
}
template <class Element>
bool Matrix::operator== (const Matrix& m) const
{
return (_length1 == m._length1)
&& (_length2 == m._length2)
&& equal (data, data+_length1*_length2, m.data);
}
#endif
Template headers have been added to the class and to each function body.
3.4 Rewrite Class Names
#ifndef MATRIX_H
#define MATRIX_H
#include <algorithm>
#include <cassert>
template <class Element>
class Matrix
//
// Provides a "2-dimensional" rectagular
// array-like structure for indexing using
// a pair of indices.
{
public:
Matrix();
Matrix (unsigned theLength1, unsigned theLength2);
Matrix<Element> (const Matrix<Element>&);
~Matrix();
const Matrix<Element>& operator= (const Matrix<Element>&);
// Indexing into the matrix: What we would like to do is allow
// myMatrix[i,j]. But C++ allows operator[] only to take a single
// parameter. But operator() can take whatever parameters we like.
// So we can write myMatrix(i,j).
Element& operator() (int i1, int i2);
const Element& operator() (int i1, int i2) const;
unsigned length1() const;
unsigned length2() const;
bool operator== (const Matrix<Element>&) const;
private:
Element* data;
unsigned _length1;
unsigned _length2;
};
template <class Element>
Matrix<Element>::Matrix()
: data(0), _length1(0), _length2(0)
{}
template <class Element>
Matrix<Element>::Matrix(unsigned theLength1, unsigned theLength2)
: _length1(theLength1), _length2(theLength2)
{
data = new Element[theLength1*theLength2];
}
template <class Element>
Matrix<Element>::Matrix(const Matrix<Element>& m)
: _length1(m._length1), _length2(m._length2)
{
data = new Element[theLength1*theLength2];
copy (m.data, m.data+theLength1*theLength2, data);
}
template <class Element>
Matrix<Element>::~Matrix()
{
delete [] data;
}
template <class Element>
const Matrix<Element>& Matrix<Element>::operator= (const Matrix<Element>& m)
{
if (this != &m)
{
if (_length1*_length2 < m._length1*m._length2)
{
delete [] data;
data = new Element[m._length1*m._length2];
}
_length1 = m._length1;
_length2 = m._length2;
copy (m.data, m.data+_length1*_length2, data);
}
return *this;
}
// Indexing into the matrix: What we would like to do is allow
// myMatrix[i,j]. But C++ allows operator[] only to take a single
// parameter. But operator() can take whatever parameters we like.
// So we can write myMatrix(i,j).
template <class Element>
Element& Matrix<Element>::operator() (int i1, int i2)
{
assert ((i1 >= 0) && (i1 < _length1));
assert ((i2 >= 0) && (i2 < _length2));
return data[i1 + _length1*i2];
}
template <class Element>
const Element& Matrix<Element>::operator() (int i1, int i2) const
{
assert ((i1 >= 0) && (i1 < _length1));
assert ((i2 >= 0) && (i2 < _length2));
return data[i1 + _length1*i2];
}
template <class Element>
unsigned Matrix<Element>::length1() const
{
return _length1;
}
template <class Element>
unsigned Matrix<Element>::length2() const
{
return _length2;
}
template <class Element>
bool Matrix<Element>::operator== (const Matrix<Element>& m) const
{
return (_length1 == m._length1)
&& (_length2 == m._length2)
&& equal (data, data+_length1*_length2, m.data);
}
#endif
Uses of the class name have been replaced.
3.5 Check Related Classes
There are no such closely related classes, so we are done.
We can use this class like this:
Matrix<int> m(4,3);
for (int i = 0; i < 4; ++i)
{
m(i,0) = i;
for (int j = 1; j < 3; ++j)
m(i,j) = m(i,j-1) + j;
}
4 Second Example – the pair class
There are times when the template form of a class is more useful than the original class might have been. For example, sometimes we need to “pair up” two different data items just so we can insert them together inside a container.
For example, suppose that we were writing a spellchecker.
A natural ADT in a spell checker would be the notion of a Replacement
- a misspelled word and the correct spelling to put in its place.
Now, Replacement
is really little more than a pair of strings. Because it’s a fairly basic concept in spell-checker-land, we could give it a descriptive name and give descriptive names to its component pieces, as shown here.
class Replacement
{
public:
Replacement () {}
Replacement (const std::string& missp,
const std::string& repl);
void setMisspelledWord (const std::string& mw);
std::string getMisspelledWord() const;
void setReplacement (const std::string& r);
std::string getReplacement() const;
void put (ostream&) const;
private:
std::string _misspelledWord;
std::string _replacement;
};
Another idea that arises in the spell checker is that of a “word occurrence”, a combination of a word and the location in a document where that word was found.
class WordOccurrence
{
public:
typedef std::streampos Location;
WordOccurrence();
//post: getLexeme()=="" && getLocation()==0
WordOccurrence (const std::string& lexeme,
Location location);
// A "lexeme" is the string of characters that has been identified
// as constituting a token.
std::string getLexeme() const {return _lexeme;}
void putLexeme (const std::string& lex) {_lexeme = lex;}
// The location indicates where in the original file the first character of
// a token's lexeme was found.
Location getLocation() const;
void putLocation (const Location&);
// Output (mainly for debugging purposes)
void put (std::ostream&) const;
private:
std::string _lexeme;
Location _location;
};
This ADT follows much the same pattern as the previous one. It’s really just a container of two items. Unlike Replacement
, the two items aren’t of the same type. But we’re basically looking at a simple pair of items, what a mathematician would call a “tuple”.
4.1 Thinking in Terms of Tuples
What’s to stop us from just writing these classes like this?
class Replacement {
public:
string first;
string second;
Replacement () {}
Replacement (string f, string s):
first(f), second(s) {}
};
class WordOccurrence {
public:
string first;
Location second;
WordOccurrence () {}
WordOccurrence (string f, Location s):
first(f), second(s) {}
};
Not much. The names aren’t as nice as our originals, we generally would prefer that all member variables should be private.
So there’s no good reason to really do this …
4.2 The pair Template
… until we consider maybe turning this pattern for containers-of-two-things into a template. Then it becomes one of those little things that’s not earth-shattering, but is still nice to have around.
// from std header file <utility>
template <class T1, class T2>
class pair {
public:
T1 first;
T2 second;
pair () {}
pair (T1 f, T2 s):
first(f), second(s) {}
};
In fact, this pair
template is part of the C++ std library, in the header <utility>
. (<utility>
also defines operators <
and ==
for pairs.)
Using pair
, we could, if we wished, redefine Replacement
and WordOccurrence
as follows
typedef pair<std::string, std::string> Replacement;
typedef pair<std::string, std::streampos> WordOccurrence;
Whether or not this is a wise choice is a judgment call. Personally, I would probably not do so, reasoning that these two abstractions are so pervasive in a typical spell checker that the documentation value obtained by accessing their components as “getMisspelledWord()
” and “getReplacement()
” is greatly preferred to simply “.first
” and “.second
”.
4.3 Applications of pair<…>
pair
gets used in situations where we need to construct a simple container-of-two-things without a lot of fuss.
For example, suppose we wanted to modify our seqSearch
template
template <class T>
int sequentialSearch
(const T a[], unsigned n, const T& x)
// Look for x within a sorted array a, containing n items.
// Return the position where found, or -1 if not found.
{
int i;
for (i = 0; ((i < n) && (a[i] < x)); i++) ;
if ((i >= n) || (a[i] != x))
i = -1;
return i;
}
template <typename T>
int seqSearch(const T list[], int listLength, T searchItem)
{
int loc;
for (loc = 0; loc < listLength; loc++)
if (list[loc] == searchItem)
return loc;
return -1;
}
so that, instead of returning the integer position at which an item was found (-1 if the item was not found), we returned a reference directly to the found item:
template <typename T>
const T& seqSearch(const T list[], int listLength, T searchItem)
{
int loc;
for (loc = 0; loc < listLength; loc++)
if (list[loc] == searchItem)
return list[loc];
return -1;
}
4.3.1 Returning the element that was found
That’s not too hard when we actually find the value we were looking for. But if we don’t find it (return -1
), then what do we do?
We need to return, not just the reference, but also a boolean value indicating whether or not we found it. If we return false, then the calling program will know not to actually look at the reference.
But wait. How can we return the reference and the boolean flag? Isn’t it true that a function can only return one value?
Of course it is. So we have two choices. The simplest choice is to turn one of those would-be return values into a simple output parameter:
template <typename T>
bool seqSearch(const T list[], int listLength, T searchItem, T& foundValue)
{
⋮
4.3.2 Returning Two Things at Once
There are times, though, when we really want that information passed out as a returned value. In those cases, we can use pair
.
template <typename T>
pair<bool, const T&> seqSearch(const T list[], int listLength,
T searchItem)
{
int loc;
for (loc = 0; loc < listLength; loc++)
if (list[loc] == searchItem)
return pair<bool, const T&>(true, list[loc]);
return pair<bool, const T&>(false, list[0]);
}
The rather hefty looking return expressions are, if you look closely, actually invoking the constructor for the data type pair<bool, const T&>
, which was declared as the functions return type. That constructor takes two parameters, hence the two expressions inside the ( ).
So we really are returning only one value from our function, but it just so happens that that one value is a pair.
Application code to use this would look something like:
pair<bool, string&> p = sequentialSearch(a, n, x);
if (p.first)
cout << "Found "
<< p.second
<< endl;
else
cout << "Not found"
<< endl;
although, thanks to C++11, we can simplify that first statement:
auto p = sequentialSearch(a, n, x);
if (p.first)
cout << "Found "
<< p.second
<< endl;
else
cout << "Not found"
<< endl;
5 Third Example: array
Wait…“array”? Don’t we already have that?
Yes, but C++ has always had a kind of love-hate relationship with its arrays. It inherited from its parent language, C, the idea that an array is just a pointer that points to repeated instances of its elements instead of just to a single one.
This has some interesting implications, both positive and negative:
-
Array declarations can be hard to read. It’s OK for static arrays. If we write
int c[3];
it’s clear that we want an array. But things get muddier for dynamically allocated arrays. When you see
int* a;
there’s no way to tell just from looking at it whether
a
is intended to hold a pointer to a single integer or to an entire array of them. We have to hunt through the code for the place where the memory is actually allocated to find out whether we seea = new int;
or
a = new int[N];
-
Arrays don’t copy naturally. If I write
array1 = array2;
that does not copy the array, but copies the address of the array (and if we aren’t careful, losing the old address in
array1
and leaving ourselves with a memory leak).This has both good and bad points.
-
Copying large arrays can take a lot of time and memory, and it’s not something we want to do indiscriminately.
-
On the other hand, sometimes we really do want to make a copy, and it’s annoying to have to write out an entire loop for an operations that is a simple assignment for almost all other data types in C++.
-
-
Arrays can’t be directly compared for equality.
-
Arrays don’t have the same parameter passing options in function calls that other data types have. With other data types, we can pass by copy, giving the function its own copy to examine, modify, and even destroy without affecting our original. When we want an output from the function, or want to avoid the time and memory cost of a copy, we can pass by reference instead.
For normal C++ arrays, we don’t have the option. We will only send an address – there is no “pass a copy” mechanism.
So, the trend in C++ is to move away from use of ordinary arrays. What we replace them with depends on the main question we always have to ask when working with arrays: do we know the size of the array we want at compilation time, or is the size computed at run time?
-
If we know the size we want when we are writing the code (compile time), then we generally used statically allocated arrays such as
int c[10];
The newer style is to use
std::array
:std::array<int, 10> c;
-
If our code needs to compute the size we want at run time, then we generally used dynamically allocated arrays such as
int* c = new int[N];
The newer style is to use
std::vector
:std::vector c;
You should already be familiar with the basic use of vectors. If not, go here for a quick review. We’ll look at the implementation of the
vector
template in a future lesson.
Right now, though, let’s look at array
as yet another example of a class template.
As shown above, we will declare standard arrays using a template instantiation:
std::array<int, 10> c;
The second template parameter must be a compile-time constant. It can be a name, instead of a literal constant, if the name has been declared as a constant:
const int MaxCustomers = 10000;
std::array<CustomerRecord, MaxCustomers> accounts;
We can initialize these arrays when we declare them:
std::array<int, 10> d = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
Once we have a std::array
, we can use it like an ordinary array:
c[i] = d[j] + 1;
but we also have some other options
c.at(i) = d.at(j) + 1;
This means the same thing as the [ ]
form, but the index value is checked first to be sure it is in the legal bounds for this array, and a run-time error is signaled if the index is illegal.
We also have some things we can do with these that we cannot do with pointer-based arrays:
-
We can fill the entire array with a value:
c.fill(0);
-
We can exchange two arrays (if they have the same element type and size):
c.swap(d);
-
We can ask how many elements are in the array:
unsigned nC = c.size();
-
We can compare them:
if (c == d) if (c != d) if (c < d) ⋮
(
c < d
is true if the two arrays have at least one pair of corresponding elements that are not equal, and the first such pair, sayc[k]
andd[k]
, havec[k] < d[k]
.)
Assignment of std arrays actually makes a copy:
c = d;
and copies are made when std arrays are passed as copy parameters to functions.
Best of all, code using std::array
should be nearly as efficient as code using pointer-based arrays. In fact, in many cases the code generated by the compiler should be nearly identical.
So how do we pull off this wonderful new class?
template < typename T, size_t N > ➀
class array
{
T elements[N]; ➁
public:
typedef T value_type; ➂
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef std::size_t size_type;
⋮
void fill(const value_type& __u); ➃
void swap(array& __other);
⋮
// Capacity.
const size_type size() const { return N; }
const size_type max_size() const noexcept { return N; }
const bool empty() const { return size() == 0; }
// Element access.
reference operator[](size_type i); ➄
const_reference operator[](size_type i) const;
reference at(size_type i);
const_reference at(size_type i) const;
reference front();
const_reference front() const;
reference back();
const_reference back() const;
pointer data() { return elements; }
const_pointer data() const { return elements; }
};
// Array comparisons.
template<typename T, std::size_t N> ➅
inline bool
operator==(const array<T, N>& left, const array<T, N>& right);
template<typename T, std::size_t N>
inline bool
operator!=(const array<T, N>& left, const array<T, N>& right);
template<typename T, std::size_t N>
inline bool
operator<(const array<T, N>& left, const array<T, N>& right);
⋮
-
➀ This is the first time we have seen a template header where one of the template parameters did not start with “typename”. There are actually two kinds of things that can be used as template parameters: data types and constants. If we want to pass a constant as a template parameter, we precede its name with its data type instead of “typename”.
In this case,
size_t
is a type declared elsewhere in the std library. It denotes some form of unsigned integer. -
➁ This is the entire data structure for
std:array
. it’s just a statically allocated array. Because it’s wrapped inside a class/struct, however, copying the struct will copy the entire array. -
➂ Something else we have not seen before. Classes (whether template or not) can provide data type names as “members” much as they provide data and function members. These can be used to provide a convenient shortcut for some names or, in the case of
std
, to help provide a uniform interface and naming scheme for all the different containers of data in the library.We access these types the same way that we access static data members and functions, via the
::
operator.typedef std::array<std::string, 100> ShortWordList; ⋮ ShortWordList commonWords = {"a", "the", "of", "in", ... }; ShortWordList::size_type nWords = commonWords.size();
If you struggle with the idea of when to use
.
and when to use::
, there are two important things to remember:- The
.
always has an object or value on the left. The::
always has a data type or namespace on the left. - We use
.
to get the properties of an object – a specific value of some class type. We use::
to get properties of the entire data type – things that do not vary from one object to another.
- The
-
➃ The rest of the class declaration lists the various functions supplied by
std::array
. You should be able to see matches for the various operations on this type that we have already discussed. -
➄ here you see the declaration of
operator[]
, the square bracket function for indexing into and array. We’ll look at the function body for this in a moment. For now, though, look at the very next line.We actually have two functions named
operator[]
. Theconst
at the end of the declaration tells us that one of these functions is used when we have astd::array
that we are allowed to change and the other is used when we have astd::array
that is a constant. The non-const version returns a reference to (address of) a data element. And code calling this function could use that address to change the value stored in the array. But the const version returns a const reference to the same element. Because it is a const reference, code calling that version could look at but not change the values stored in the array.This idea of providing distinct, overloaded function pairs for use on const and non-const data is a very common practice in C++.
-
➅ Various relational operators are then declared, outside of the class declaration itself.
So, how does all this work? Let’s look at the critical functions for accessing data. For example, to support operations like
c[i] = d[j] + 1;
we use
template <typename T, size_t N>
array<T,N>::reference array<T,N>::operator[]
(array<T,N>::size_type i)
{
return elements[i];
}
After all the elaborate syntax of the template header and the function declaration itself, the body is amazingly simple.
And the const version looks pretty much the same:
template <typename T, size_t N>
array<T,N>::const_reference array<T,N>::operator[]
(array<T,N>::size_type i) const
{
return elements[i];
}
The at
functions just add a little bit of checking:
template <typename T, size_t N>
array<T,N>::reference array<T,N>::operator[]
(array<T,N>::size_type i)
{
if (i >= 0 && i < N)
return elements[i];
else
...abort with a run-time error...
}
Now, I have left out a little bit of information on the std::array
. Most of that has to do with “iterators”, which we will look at very shortly.
1: : The only difference between a class
and a struct
in C++ is that the members of a struct are, by default, public, but the members of a class are, by default, private.
2: : Your textbook also develops a similar matrix template, but it uses some techniques that we have not covered yet, so for illustrative purposes I’ve decided to keep this example. Still, after you’ve read through this one, you may find it interesting to return to section 1.7 of your textbook and comparing to his version.