Class Templates

Steven J. Zeil

Last modified: Oct 26, 2023
Contents:

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

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 strings, 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 PayrollRecords. 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.

seq1.cpp
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 …

seq2.cpp
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

seq3.cpp
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.

seq4.cpp
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:

  1. 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.)

  2. Combine everything from the class’s .h and .cpp files into the .h file.

  3. 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.

  4. Change all uses of the class name, except for constructor and destructor names, to theClassName<templateParams>.

  5. 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

  1. Start with a working non-template class using template parameter names but employing typedefs to supply replacements for the parameter names.

  2. Combine everything from the class’s .h and .cpp files into the .h file.

  3. 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.

  4. Change all uses of the class name, except for constructor and destructor names, to the ClassName<templateParams>’.

  5. 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…

matrix0.h
#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
matrix0.cpp
#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

matrix1.h
#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

matrix2.h
#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

matrix3.h
#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.

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

search1.cpp
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:

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?

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:

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);

     ⋮

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.