Home CPSC 340

Templates

Overview

So far in this class, we have implemented data structures that hold a specific type of data, such as ints, strings, doubles etc. For example, the Stack example from last class stores ints.

If we want to change the data structure to store something else, we would have to rewrite the data structure class to hold some new type of data. For example, for the Playlist project, you had to modify a linked list class to hold song information, and for the Calculator project, you had to modify a stack to hold doubles.

C++ offers a way of avoiding this by using templates. Templates allow us to write code that works with lots of different types. So we can write a linked list class once, and then be able to use it to store ints, doubles, songs, or anything else we can want.

The basic idea is that we have type parameters for a class. Like regular parameters, type parameters can have different values. Type parameters, however, are types instead of values.

Then when we create an object of a class, we pass the type we want to store and a class of the right types is created.


Template Classes

To create a template class, we start by declaring the type parameter using two new keywords, template and typename:

template <typename type>

The template keyword specifies that we are making a class that has a template parameter. The typename keyword says that the parameter is a type. The word "type" is the name of the type parameter and can be any name you like.

After that line comes a class declaration like normal. The only difference is that now we can use "type" to signify a type that will be replaced with a real one later on.


template <typename type>
class Thing {
  public:
    Thing(type v) {
      value = v;
    }

    void print() const {
      cout << value;
    }

  private:
    type value;
};

This class can now store essentially any type of data and print it out using the print function. Now if we want to create objects of this class, we will need to fill in the type parameter. This is done by specifying the type inside of angle brackets after the class name:


Thing<int> thing1(5);

If we want the Thing class to store something else instead, we can simply replace the type parameter to be something else. Likewise, we can also create multiple Thing objects that have different type parameters. If we were writing a program that needed a linked list of numbers and a linked list of strings, templates allow us to write just one linked list class and use it for both.


Thing <float> thing2(5.5);
Thing <const char*> thing3("Hello");

This example shows the ListStack class written using a template parameter. The differences are the template line at the start of the class, the fact that we use "type" throughout the class to refer to the type of data in the stack and that we now specify the type of data we want when we create the ListStack object.


Template Functions

We can also use templates to write functions that work on multiple types of data. For example, say we want to write a function that compares two numbers and returns the one that is largest. We can write such a function for integers like so:


int max(int a, int b) {
  if(a > b) {
    return a;
  } else {
    return b;
  }
}

If we also wanted to be able to use this function on doubles, or chars, or even BigInts, we would not be able to. This is because the function is declared as taking two integers, and if we pass anything else the compiler will give an error. To get around this, we can make this function templated just like we would for a class:


template <typename type>
type max(type a, type b) {
  if(a > b) {
    return a;
  } else {
    return b;
  }
}

Now this function can be called for ints, chars, doubles and even BigInt objects. When calling a templated function, you do not need to specify the template parameter, though you can. This example shows the the max function written with a template.


Templates & Operators

The reason that the max function above works with BigInt objects is because the BigInt class overloaded the comparison operators including >. When we try to make an object of a template class, or call a template function, the compiler basically plugs in the desired type and tries to compile it. If the type is used in a way that it doesn't support (such as calling an operator or function that doesn't work), then the compiler gives an error.

For example, say we have our "Thing" class above that holds any type of data and prints it out. Because this class uses "cout >>" to print the data, the types it stores have to support the output operator or the compiler will complain. This is demonstrated in this example.

The upshot of this is that we often have to overload operators in our classes to make them work properly in templated classes and functions.


File Issues

In the classes that we have written so far, we have split them up into a header file (.h) and a source file (.cpp). This is generally a good idea and is done for almost all C++ code.

The designers of C++ intended for this to be done with templates as well. Unfortunately, this turned out to be very difficult for compilers to do, and hardly any (including g++) do support it.

When we create a template class, and give it a type, the compiler has to fill in the class with the correct type for every function. So the compiler needs the complete definition of the class including the code for every function. Just having the names, which are normally just in the header file is not enough.

This means that if we make a class that is templated, we cannot split it up into two files like we have been doing. We have to put everything in the header file, both the names of the functions and the actual code for them.

This example demonstrates how this looks.


Integer Templates

In addition to type parameters, we can also use ints as template parameters. This allows us to customize a class or function based on a number. For example, the example of the stack based on the array currently takes the size in from the constructor, and uses dynamic allocation and de-allocation.

To avoid needing to do the dynamic memory allocation, we could instead make the size a template parameter. When we do this, we specify the size when we create the object and the compiler fills in the size everywhere for us.

This example shows the ArrayStack with an int template parameter. This example also demonstrates that we can have multiple template parameters at one time. An unlimited number can be used, but it's very rare to have more than a few.

Copyright © 2018 Ian Finlayson | Licensed under a Creative Commons Attribution 4.0 International License.