Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

Template syntax

The template keyword tells the compiler that the following class definition will manipulate one or more unspecified types. At the time the object is defined, those types must be specified so the compiler can substitute them.

Here’s a small example to demonstrate the syntax:

//: C16:Stemp.cpp
// Simple template example
#include "../require.h"
#include <iostream>
using namespace std;

template<class T>
class Array {
  static const int size = 100;
  T A[size];
public:
  T& operator[](int index) {
    require(index >= 0 && index < size);
    return A[index];
  }
};

int main() {
  Array<int> ia;
  Array<float> fa;
  for(int i = 0; i < 20; i++) {
    ia[i] = i * i;
    fa[i] = float(i) * 1.414;
  }
  for(int j = 0; j < 20; j++)
    cout << j << ": " << ia[j]
         << ", " << fa[j] << endl;
} ///:~

You can see that it looks like a normal class except for the line

template<class T>

which says that T is the substitution parameter, and it represents a type name. Also, you see T used everywhere in the class where you would normally see the specific type the container holds.

In Array, elements are inserted and extracted with the same function, the overloaded operator[ ]. It returns a reference, so it can be used on both sides of an equal sign. Notice that if the index is out of bounds, the require( ) function is used to print a message. This is actually a case where throwing an exception is more appropriate, because then the class user can recover from the error, but that topic is not covered until Chapter XX.

In main( ), you can see how easy it is to create Arrays that hold different types of objects. When you say

Array<int> ia;
Array<float> fa;

the compiler expands the Array template (this is called instantiation) twice, to create two new generated classes, which you can think of as Array_int and Array_float. (Different compilers may decorate the names in different ways.) These are classes just like the ones you would have produced if you had performed the substitution by hand, except that the compiler creates them for you as you define the objects ia and fa. Also note that duplicate class definitions are either avoided by the compiler or merged by the linker.

Non-inline function definitions

Of course, there are times when you’ll want to have non-inline member function definitions. In this case, the compiler needs to see the template declaration before the member function definition. Here’s the above example, modified to show the non-inline member definition:

//: C16:Stemp2.cpp
// Non-inline template example
#include "../require.h"

template<class T>
class Array {
  static const int size = 100;
  T A[size];
public:
  T& operator[](int index);
};

template<class T>
T& Array<T>::operator[](int index) {
  require(index >= 0 && index < size,
    "Index out of range");
  return A[index];
}

int main() {
  Array<float> fa;
  fa[0] = 1.414;
} ///:~

Notice that in the member function definition the class name is now qualified with the template argument type: Array<T>. You can imagine that the compiler does indeed carry both the name and the argument type(s) in some decorated form.

Header files

Even if you create non-inline function definitions, you’ll generally want to put all declarations and definitions for a template in a header file. This may seem to violate the normal header file rule of “Don’t put in anything that allocates storage” to prevent multiple definition errors at link time, but template definitions are special. Anything preceded by template<...> means the compiler won’t allocate storage for it at that point, but will instead wait until it’s told to (by a template instantiation), and that somewhere in the compiler and linker there’s a mechanism for removing multiple definitions of an identical template. So you’ll almost always put the entire template declaration and definition in the header file, for ease of use.

There are times when you may need to place the template definitions in a separate cpp file to satisfy special needs (for example, forcing template instantiations to exist in only a single Windows dll file). Most compilers have some mechanism to allow this; you’ll have to investigate your particular compiler’s documentation to use it.

The stack as a template

Here is the container and iterator from Istack.cpp, implemented as a generic container class using templates:

//: C16:Stackt.h
// Simple stack template
#ifndef STACKT_H
#define STACKT_H
template<class T> class StacktIter; // Declare

template<class T>
class Stackt {
  static const int ssize = 100;
  T stack[ssize];
  int top;
public:
  Stackt() : top(0) { stack[top] = 0; }
  void push(const T& i) {
    if(top < ssize) stack[top++] = i;
  }
  T pop() {
    return stack[top > 0 ? --top : top];
  }
  friend class StacktIter<T>;
};

template<class T>
class StacktIter {
  Stackt<T>& s;
  int index;
public:
  StacktIter(Stackt<T>& is)
    : s(is), index(0) {}
  T& operator++() { // Prefix form
    if (index < s.top - 1) index++;
    return s.stack[index];
  }
  T& operator++(int) { // Postfix form
    int returnIndex = index;
    if (index < s.top - 1) index++;
    return s.stack[returnIndex];
  }
};
#endif // STACKT_H ///:~

Notice that anywhere a template’s class name is referred to, it must be accompanied by its template argument list, as in Stackt<T>& s . You can imagine that internally, the arguments in the template argument list are also being decorated to produce a unique class name for each template instantiation.

Also notice that a template makes certain assumptions about the objects it is holding. For example, Stackt assumes there is some sort of assignment operation for T inside the push( ) function. You could say that a template “implies an interface” for the types it is capable of holding.

Here’s the revised example to test the template:

//: C16:Stackt.cpp
// Test simple stack template
#include "Stackt.h"
#include "../require.h"
#include <iostream>
using namespace std;

// For interest, generate Fibonacci numbers:
int fibonacci(int n) {
  const int sz = 100;
  require(n < sz);
  static int f[sz]; // Initialized to zero
  f[0] = f[1] = 1;
  // Scan for unfilled array elements:
  int i;
  for(i = 0; i < sz; i++)
    if(f[i] == 0) break;
  while(i <= n) {
    f[i] = f[i-1] + f[i-2];
    i++;
  }
  return f[n];
}

int main() {
  Stackt<int> is;
  for(int i = 0; i < 20; i++)
    is.push(fibonacci(i));
  // Traverse with an iterator:
  StacktIter<int> it(is);
  for(int j = 0; j < 20; j++)
    cout << it++ << endl;
  for(int k = 0; k < 20; k++)
    cout << is.pop() << endl;
} ///:~

The only difference is in the creation of is and it: You specify the type of object the stack and iterator should hold inside the template argument list.

Constants in templates

Template arguments are not restricted to class types; you can also use built-in types. The values of these arguments then become compile-time constants for that particular instantiation of the template. You can even use default values for these arguments:

//: C16:Mblock.cpp
// Built-in types in templates
#include "../require.h"
#include <iostream>
using namespace std;

template<class T, int size = 100>
class Mblock {
  T array[size];
public:
  T& operator[](int index) {
    require(index >= 0 && index < size);
    return array[index];
  }
};

class Number {
  float f;
public:
  Number(float ff = 0.0f) : f(ff) {}
  Number& operator=(const Number& n) {
    f = n.f;
    return *this;
  }
  operator float() const { return f; }
  friend ostream&
    operator<<(ostream& os, const Number& x) {
      return os << x.f;
  }
};

template<class T, int sz = 20>
class Holder {
  Mblock<T, sz>* np;
public:
  Holder() : np(0) {}
  T& operator[](int i) {
    require(i >= 0 && i < sz);
    if(!np) np = new Mblock<T, sz>;
    return np->operator[](i);
  }
};

int main() {
  Holder<Number, 20> h;
  for(int i = 0; i < 20; i++)
    h[i] = i;
  for(int j = 0; j < 20; j++)
    cout << h[j] << endl;
} ///:~

Class Mblock is a checked array of objects; you cannot index out of bounds. (Again, the exception approach in Chapter XX may be more appropriate than assert( ) in this situation.)

The class Holder is much like Mblock except that it has a pointer to an Mblock instead of an embedded object of type Mblock. This pointer is not initialized in the constructor; the initialization is delayed until the first access. You might use a technique like this if you are creating a lot of objects, but not accessing them all, and want to save storage.

Contents | Prev | Next


Contact: webmaster@codeguru.com
CodeGuru - the website for developers.