Class Templates Tutorial

The following tutorial is adapted from Professional C++ Chapter 11, "Writing Generic Code with Templates."

What are Templates?

Most computer languages, including C++, provide support for procedures or functions that allow you to write algorithms that are independent of specific values and can thus be reused for many different values. For example, the sqrt() function in C and C++ calculates the square root of a value supplied by the caller. A square root function that calculated only the square root of one number, like four, would not be particularly useful! The sqrt() function is written in terms of a parameter, which is a stand-in for whatever value the caller passes. Computer scientists say that functions parameterize values.

Templates (called Generics in Java) take the concept a step further to allow you to parameterize on types as well as values. Recall that types in C++ include primitives such as int and double, as well as user-defined classes. With templates, you can write code that is independent not only of the values it will be given, but of the types of those values as well! For example, instead of writing separate queue classes to store ints, Packets, or any other object, you can write one queue class definition that can be used for any of those types.

C++ supports both class templates and function templates. This tutorial will cover the basics of class templates.

Writing a Class Template

Suppose you want a generic two-dimensional grid class that you could use as a game-board, as a spreadsheet, or for anything else that requires a 2-D grid. In order to make it general-purpose, you should be able to store elements of any type in the grid. The class should provide methods to set and retrieve an element at any location in the grid, and could be implemented as a two-dimensional array.

The Grid Class Definition

Here’s the class outline:

template <typename T>
class Grid
{
};

The first line says that the following class definition is a template on one type. Note that, for historical reasons, you can use the keyword class in place of typename. You might see this usage in programs or books, but it means exactly the same thing as using typename. Just as we use variables to represent function parameters, we use variables to represent type parameters. In this case we call the type parameter T, but there’s nothing special about that name. You could call it someVariableName if you wanted. In the same way that a function body can refer to the parameter variable names, a class definition can refer to the type variable name.

Here’s the class definition with the constructors, destructor, and assignment operator filled in:


template <typename T>
class Grid
{
public:
   static const int kDefaultWidth = 10;
   static const int kDefaultHeight = 10;

   Grid(int inWidth = kDefaultWidth, int inHeight = kDefaultHeight);
   Grid(const Grid<T>& src);
   ~Grid();
   Grid<T>& operator=(const Grid<T>& rhs);
};

Note that the copy constructor takes as its src argument const Grid<T>&, not const Grid&. That’s because, in a class template, what you used to think of as the class name Grid is actually the template name. When you want to talk about actual Grid classes or types, you discuss them as instantiations of the Grid class template for a certain type. You haven’t specified the real type yet, so you must use a stand-in template parameter, T, for whatever type might be used later. Thus, when you need to refer to a type for a Grid object as a parameter to, or return value from, a method, you must use Grid<T>. You can see this change also in the assignment operator.

Here’s the full class definition:

template <typename T>
class Grid
{
public:
   Grid(int inWidth = kDefaultWidth, int inHeight = kDefaultHeight);
   Grid(const Grid<T>& src);
   ~Grid();
   Grid<T>& operator=(const Grid<T>& rhs);

   void setElementAt(int x, int y, const T& inElem);
   T& getElementAt(int x, int y);
   const T& getElementAt(int x, int y) const;

   int getHeight() const { return mHeight; }
   int getWidth() const { return mWidth; }
   static const int kDefaultWidth = 10;
   static const int kDefaultHeight = 10;

protected:
   void copyFrom(const Grid<T>& src);
   T\*\* mCells;
   int mWidth, mHeight;
};

Note that setElementAt() takes a reference to an element of type T. This type T is a placeholder for whatever type the user specifies when she instantiates the template (creates an object of the class for a specific type). Similarly, getElementAt() returns a reference to type T, and mCells is a pointer to a two-dimensional array of T elements.

The Grid Class Implementation

The key points when writing template classes are that the template <typename T> specifier must precede each method definition, and the class name before the :: is Grid<T> not Grid. For example, the constructor looks like this:


template <typename T>
Grid<T>::Grid(int inWidth, int inHeight) : mWidth(inWidth), mHeight(inHeight)
{
   mCells = new T\* [mWidth];
   for (int i = 0; i < mWidth; i++) {
      mCells[i] = new T[mHeight];
   }
}

Note the use of T as a stand-in for whatever type the user specifies when she instantiates the template. If it confuses you, just substitute a real type like int in your head to make sense of the code.

Here’s another method implementation:

template <typename T>
T& Grid<T>::getElementAt(int x, int y)
{
   return (mCells[x][y]);
}

You can download the full class definition and implementation here.

Using the Grid Template

When you want to create grid objects, you cannot use Grid alone as a type; you must specify the type that will be stored in that grid. Here is an example:

#include "Grid.h"

int main(int argc, char\*\* argv)
{
   Grid<int> myIntGrid; // declares a grid that stores ints
   myIntGrid.setElementAt(0, 0, 10);
   int x = myIntGrid.getElementAt(0, 0);

   Grid<int> grid2(myIntGrid);
   Grid<int> anotherIntGrid = grid2;

   return (0);
}

Note that the type of mIntGrid, grid2, and anotherIntGrid is Grid<int>. You cannot store doubles or strings in these grids; the compiler will generate an error if you try to do so.

The type specification is important: neither of the following two lines compiles:

   Grid test; // WILL NOT COMPILE
   Grid<> test; // WILL NOT COMPILE

If you want to declare a function or method that takes a Grid object, you must specify the type stored in that grid as part of the Grid type:

void processIntGrid(Grid<int>& inGrid)
{
   // body omitted for brevity
}

You can also dynamically allocate Grid template instantiations on the heap:

   Grid<int>\* myGridp = new Grid<int>();
   myGridp->setElementAt(0, 0, 10);
   x = myGridp->getElementAt(0, 0);

   delete myGridp;

You can download the test file here.

Distributing Template Code between Files

Because they are "templates" for the compiler to generate the actual methods for the instantiated types, both template class definitions and method definitions must be available to the compiler in any source file that uses them. In this sense, methods of a template class are similar to inline methods. In order to obtain this inclusion, you can put the method definitions directly in the same header file as the class definition itself, or you can put the method definitions in a separate header or source file and #include that file at the bottom of the class definition file.

Comments:

The code examples are microscopic in Firefox on RHEL4 with KDE.

Posted by Griff Miller on August 24, 2007 at 07:20 AM MDT #

Post a Comment:
Comments are closed for this entry.
About

Nick Solter is a software engineer and author living in Colorado.

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today