Wrox Press C++ Tutorial


Class Templates

We saw back in Chapter 5 that we could define a function template which would automatically generate functions, varying only in the type of arguments accepted or in the type of value returned. C++ has a similar mechanism for classes. A class template is a sort of 'recipe' for a class. As you can see from the diagram, it's like the function template - it's a definition of a class that you use by specifying your choice of type for the variable parameter (T in this case). Doing this generates a particular instance of a class.

An appropriate class definition is generated when you instantiate an object of a template class for a particular type, so you can generate any number of different classes from one class template. We can see how this works in practice by looking at an example.

Defining a Class Template

We'll choose a simple example to illustrate how you define and use a class template, and we won't complicate things by worrying too much about possible errors that can arise if it's misused. Let's suppose we want to define classes which can store a number of data samples of some kind, and each class is to provide a max() function to calculate the maximum sample of those stored. This function will be similar to the one we saw in the function template discussion in Chapter 5. We can define a class template that will generate a class Samples for whatever type we want:

template <class T>
class Samples
{
   private:
      T values[100];       // Array to store samples
      int free;            // Index of free location in values

   public:
      // Constructor definition to accept an array of samples
      Samples(T vals[], int count)
      {
         free = count<100? count:100; // Don't exceed the array
         for(int i=0; i<free; i++)
            values[i] = vals[i]; // Store count number of samples
      }

      // Constructor to accept a single sample
      Samples(T val)
      {
         values[0] = val;     // Store the sample
         free = 1;            // Next is free
      }

      // Default constructor
      Samples()
      {
         free = 0;            // Nothing stored, so first is free
      }

      // Function to add a sample
      bool Add(T& val)
      {
         bool OK = free<100; // Indicates there is a free place
         if(OK)
            values[free++] = val; // OK true, so store the value
         return OK;
      }

      // Function to obtain maximum sample
      T Max()
      {
         T theMax = values[0];     // Set first sample as maximum
         for(int i=1; i<free; i++)  // Check all the samples
         if(values[i]>theMax)
            theMax = values[i];       // Store any larger sample
         return theMax;
      }
};

To indicate we're defining a template rather than a straightforward class definition, we insert the keyword template and the type parameter, T, between angled brackets, just before the keyword class and the class name, Samples. This is the same syntax that we used to define a function template back in Chapter 5. The parameter T is the type variable that you'll replace by a specific type when you declare a class object. Wherever the parameter T appears in the class definition, it will be replaced by the type that you specify in your object declaration; this creates a class definition corresponding to this type. You can specify any type (a basic data type or a class type), but it has to make sense in the context of the class template, of course. Any class type that you use to instantiate a class from a template must have all the operators defined that the member functions of the template will use with such objects. If your class hasn't implemented operator>(), for example, it will not work with our class template above. In general, you can specify multiple parameters in a class template if you need them. We'll come back to this possibility a little later in the chapter.

Getting back to our example, the type of the array in which to store the samples is specified as T. The array will therefore be an array of whatever type you specify for T when you declare a Samples object. As you can see, we also use the type T in two of the constructors for the class, as well as in the Add() and Max() functions. Each of these occurrences will also be replaced when you instantiate a class object. The constructors we've defined support the creation of an empty object, an object with a single sample and an object initialized with an array of samples. The Add() function allows samples to be added to an object, one at a time. You could also overload this function to add an array of samples. The class template includes some elementary provision to prevent the capacity of the values array being exceeded in the Add() function, and in the constructor that accepts an array of samples.

As we said earlier, in theory you can create objects of Samples classes that will handle any data type: int, double, or any class type that you've defined. In practice, this doesn't mean it will necessarily compile and work as you expect. It all depends on what the template definition does, and usually a template will only work for a particular range of types. For example, the Max() function implicitly assumes that the > operator is available for whatever type is being processed. If it isn't, your program will not compile. Clearly, you'll usually be in the position of defining a template that works for some types but not others, but there's no way you can restrict what type is applied to a template.

Template Member Functions

You may want to place the definition of a class template member function outside of the template definition. The syntax for this isn't particularly obvious, so let's look at how you do it. The function declaration appears in the class template definition in the normal way. For instance:

template <class T> 
class Samples 
{
   // Rest of the template definition...
   T Max();// Function to obtain maximum sample
   // Rest of the template definition...
}

This declares the Max() function within the class. You now need to create a separate function template for the definition of the member function. You must use the template class name plus the parameters in angled brackets to identify the class to which the function belongs:

template<class T>
T Samples<T>::Max()
{
   T theMax = values[0];        // Set first sample as maximum
   for(int i=1; i<free; i++)    // Check all the samples
      if(values[i]>theMax)
         theMax = values[i];    // Store any larger sample
   return theMax;
}

You saw the syntax for a function template back in Chapter 5. The function template definition here should have the same parameters as the class template. There's just one in this case, but in general there can be several. Note how you only put the parameter name, T, along with the class name before the scope resolution operator. This is necessary - the parameters are fundamental to the identification of the class to which a function, produced from the template, belongs. Your type is plugged into the class template to generate the class definition, and into the function template to generate the definition for the Max() function for the class. Each class that's produced from the class template needs to have its own definition for the function Max().

Defining a constructor or a destructor outside of the class template definition is very similar. We could write the definition of the constructor accepting an array as:

template<class T>
Samples<T>::Samples(T vals[], int count)
{
   free = count<100? count:100;   // Don't exceed the array
   for(int i=0; i<m_Free; i++)
      values[i] = vals[i];       // Store count number of samples
}

The class to which the constructor belongs is specified in the template in the same way as for an ordinary member function. Note that the constructor name doesn't require the parameter specification. You only use the parameter with the class name preceding the scope resolution operator.

Creating Objects from a Class Template

When we used a function defined by a function template, the compiler was able to generate the function from the types of the arguments used. The type parameter for the function template was implicitly defined by the specific use of a particular function. Class templates are a little different. To create an object based on a class template, you must always specify the type parameter following the class name in the declaration.

For example, to declare a Samples object to handle samples of type double, you could write the declaration as:

Samples<double> MyData(10.0);

This defines a Samples object that can store samples of type double, and the object is created with one sample stored with the value 10.0.

Try It Out - Class Templating

You could create a Samples object that stores our Box objects. This will work because the Box class implements the operator>() function to overload the greater-than operator. We could exercise the class template with the main() function in the following listing:

// Ex7_07.cpp
// Trying out a class template
#include <iostream>
using namespace std;

// Put the Box class definition here

// Put the Samples class template definition here

int main()
{
   Box Boxes[] = {                    // Create an array of boxes
                   Box(8.0, 5.0, 2.0), // Initialize the boxes...
                   Box(5.0, 4.0, 6.0),
                   Box(4.0, 3.0, 3.0)
                  };

   // Create the Samples object to hold Box objects
   Samples<Box> MyBoxes(Boxes, sizeof Boxes/sizeof Box);
   Box MaxBox = MyBoxes.Max();           // Get the biggest box
   cout << endl                          // and output its volume
        << "The biggest box has a volume of "
        << MaxBox.Volume()

        << endl;
   return 0;
}

Here we create an array of three Box objects and then use this array to initialize a Samples object that can store Box objects. The declaration of the Samples object is basically the same as it would be for an ordinary class, but with the addition of the type parameter in angled brackets following the template class name.

The two pieces of code you should replace the comments with are:

The program will generate the following output:

You could try modifying the example by implementing function member definitions outside the template, and perhaps seeing what happens when you instantiate classes by using the template with various other types.

You might be surprised at what happens if you add some output statements to the class constructors. The constructor for the Box is being called 103 times! Look at what we are doing in main(). First we create an array of 3 Box objects, so that's 3 calls. We then create a Samples object to hold them, but a Samples object contains an array of 100 variables of type Box, so we call the constructor another 100 times.

Class Templates with Multiple Parameters

Using multiple type parameters in a class template is a straightforward extension of the example using a single parameter, which we have just seen. You can use each of the type parameters wherever you want in the template definition. For example, you could define a class template with two type parameters:

template<class T1, class T2>
class ExampleClass
{
   // Class data members
   private:
      T1 value1;
      T2 value2;
      // Rest of the template definition...
};

The types of the two class data members shown will be determined by the types you supply for the parameters when you instantiate an object.

The parameters in a class template aren't limited to types. You can also use parameters that require constants or constant expressions to be substituted in the class definition. In our Samples template, we arbitrarily defined the values array with 100 elements. We could, however, let the user of the template choose the size of the array when the object is instantiated, by defining the template as:

template <class T, int Size> class Samples
{
   private:
      T values[Size];      // Array to store samples
      int free;            // Index of free location in values

   public:
      // Constructor definition to accept an array of samples
      Samples(T vals[], int count)
      {
         free = count<Size? count:Size; // Don't exceed the array
         for(int i=0; i<m_Free; i++)
            values[i] = vals[i]; // Store count number of samples
      }

      // Constructor to accept a single sample
      Samples(T val)
      {
         values[0] = val;   // Store the sample
         free = 1;          // Next is free
      }

      // Default constructor
      Samples()
      {
         free = 0;          // Nothing stored, so first is free
      }

      // Function to add a sample
      bool Add(T& val)
      {
         bool OK = free<Size; // Indicates there is a free place
         if(OK)
            values[free++] = val; // OK true, so store the value
         return OK;
      }

      // Function to obtain maximum sample
      T Max()
      {
         T theMax = values[0];     // Set first sample as maximum
         for(int i=1; i<free; i++) // Check all the samples
            if(values[i]>theMax)
               theMax = values[i]; // Store any larger sample
         return theMax;
      }
};

The value supplied for Size when you create an object will replace the appearance of the parameter throughout the template definition. Now we can declare the Samples object from the previous example as:

Samples<Box, 3> MyBoxes(Boxes, sizeof Boxes/sizeof Box);

Since we can supply any constant expression for the Size parameter, we could also have written this as:

Samples<Box, sizeof Boxes/sizeof Box>
MyBoxes(Boxes, sizeof Boxes/sizeof Box);

You need to be careful with expressions that involve comparison operators. A statement such as:

Samples<aType, x>y? 10:20> MyType();      // Wrong!

will not compile correctly because the > in the expression will be interpreted as a right angled bracket. Instead, you should write this as:

Samples<aType, (x>y? 10:20)> MyType();    // OK

The parentheses make sure that the expression for the second template argument doesn't get mixed up with the angled brackets.


© 1998 Wrox Press