C++ Templates

Brad Vander Zanden


  1. Motivation for Templates: Allow the programmer to write type-independent classes and functions. In other words, the programmer writes the code once and it works with different types.
    1. Example: Dlist--There are many types that we might want to store in dlists, such as integers, strings, or payroll records. The methods for manipulating dlists are the same for each of these types. The only difference is the type of elements stored in the list.
    2. In C++, templates are like macros. The type arguments you provide when you declare a variable are literally substituted for the type parameters in the template and then the template code is compiled.
    3. C++ allows both primitive types and classes as type parameters.
    4. C++ allows constants to be included as type parameters (see below for an example)
  2. Using Templates
    1. Class Templates: To declare a variable to have a template class type, you need to provide the name of the class and any type parameters that the class expects. For example:
             Dlist<int> a;          // A dlist of ints
             Dlist<PayrollRec *> b; // A dlist of pointers to payroll records
             HashTable<int, string> x; // A hash table in which the key is
                                       // an integer and the value is a string
      
      1. Once the variable is declared, you can forget that the variable is a template class and simply use its methods as normal. For example:
        	  a.insertBeforeCursor(3);  // inserts 3 before the cursor
        	  
        	  PayrollRec *p = new PayrollRec("Brad Vander Zanden", 20000.00);
        	  b.insertBeforeCursor(p);  // inserts the pointer to the new
        	                             // payroll record before the cursor
                  string y = x.find(4);    // return the value associated with 4
        
    2. Function Templates: The C++ compiler automatically instantiates a function template based on the types of the passed-in arguments so you don't even have to know whether or not a function is a template. The only time we will use function templates is in template classes so you need not worry about function templates in this class (i.e., each method in a template class is implemented as a template).
  3. Writing Class Templates
    1. Syntax
      1. The class declaration is preceded by a line of the form:
        	
        	   template <class Type1, class Type2, ..., class Typen>
        
        where template and class are keywords and Type1, ..., Typen are the names of the type parameters

        1. you can use typename rather than class.
        2. You typically use class if you always expect the type parameter to be a class and typename if the type parameter might be either a class or a primitive type.

      2. Every place in the class where you would normally have a specific type, you substitute the appropriate type parameter
      3. Example: Here is a class declaration for a dynamic array template class
        	 // Element types are assumed to have defined operator=
        	 template <class Element, int ArraySize = 12>
        	 class DynArray {
        	  public:
        	    // operations performed on arrays
        	    DynArray ( int sz = ArraySize );
        	    ~DynArray();
        	    // return the value at location index
        	    Element getValue(int index);
        	    void setValue(int index, Element value);
        	    int getSize();
        
        	  protected:
        	    int size;
        	    Element *data;
        	 };
                 #include "DynArray.cpp"
        
        1. Note that you can use constants as well as type parameters
        2. I have never seen or used anything but type parameters in templates
        3. As an example of why you might use a constant, you might want to allow the user to create stack allocated arrays of a fixed size. The only way to do that is to use an integer constant. For example:
          	template <class Element, int StackSize = 12>
          	 class Stack {
          	  public:
          	    ...
          	  protected:
          	    int size;
          	    Element data[StackSize];
          	 };
          	
        4. This ability to constrain the size of stack-allocated arrays is appealing, but unfortunately C++ must generate and compile a completely new set of functions for each instantiation of the class, which can lead to considerable code bloat and compilation time.
    2. How to think of class instantiation: When a class is instantiated with actual types, the actual types replace the type parameters in the template. In effect the type parameters are placeholders that get replaced with the appropriate type. For each different set of types provided by the user, C++ creates a complete set of code for the class, with the type parameters replaced with the actual types.
    3. Writing methods for a template class:
      1. In the .h file: This is the easy, less cluttered way is to put the method bodies into the .h file with the class declaration. Since template code must be included in every file that uses it (see below), rather than being compiled, this is one situation where it is permissable to put the method bodies in the .h file. However, putting method bodies in a class declaration represents a strong request to the C++ compiler to inline the method bodies, which can result in code bloat if the method bodies are large. Thus if you have large method bodies, it is probably still best to put the methods in a separate .cpp file.

        Here is an example of the DynArray class with the method bodies embedded in the class declaration:

        #include <iostream>
        using namespace std;
        
         template <class Element, int ArraySize = 12>
         class DynArray {
           public:
           // operations performed on arrays
           DynArray ( int sz = ArraySize ) {
            size = sz;
            data = new Element[sz];
           }
        
           ~DynArray() {
             delete [] data;
           }
        
           // return the value at location index
           Element getValue(int index) {
             if (index < 0) {
               cerr << "Index " << index << " is negative" << endl;
               exit(1);
             }
             else if (index >= size) {
               cerr << "Index " << index << " is out of range" << endl;
               cerr << "The current size of the array is " << size << endl;
               exit(1);
             }
             else
               return data[index];
           }
        
           void setValue(int index, Element value) {
             if (index < 0) {
               cout << "Index " << index << " is negative" << endl;
               exit(1);
             }
             if (index >= size) {
               int old_size = size;
               Element *old_data = data;
               int i;
        
               while (index >= size) 
                 size *= 2;
                 data = new Element[size];
                 for (i = 0; i < old_size; i++) 
                   data[i] = old_data[i];
                 delete [] old_data;
             }
             data[index] = value;
           }
        
           int getSize() {
             return size;
           }
        
           protected:
             int size;
             Element *data;
        };
        
      2. In a separate .cpp file: The methods in a template class must also be written using the template syntax. If a class is declared as:
               template <class Type1, ..., class Typen>
               class Foo {
                 Type1 y(Type2 arg1);
               }
        
        then y would be declared as
               template <class Type1, ..., class Typen>
               Type1 Foo<Type1, ..., Typen>::y(Type2 arg1) { ... }
        
        In other words, all the pageantry with the template line must be repeated verbatim and you have to write the class name as if it were being instantiated with the generic types.

        Here here is a sample definition of the method bodies for Dynarray when they are placed in a separate .cpp file:

        #include <iostream>
        #include <cstdlib>
        
        using namespace std;
        
                // note that you don't say "sz = ArraySize" in the constructor.
        	// You only say that in the .h file
        	template <class Element, int ArraySize>
        	DynArray<Element, ArraySize>::DynArray( int sz )
        	{
        	  size = sz;
        	  data = new Element[sz];
        	}
        
        	template <class Element, int ArraySize>  
        	DynArray<Element, ArraySize>::~DynArray()
        	{
        	  delete [] data;
        	}
          
                // set the value at location index
        	template <class Element, int ArraySize>  
        	void DynArray<Element, ArraySize>::setValue(int index, Element value) 
        	{
        	  if (index < 0) {
        	    cout << "Index " << index << " is negative" << endl;
        	    exit(1);
        	  }
        	  if (index >= size) {
        	    int old_size = size;
        	    Element *old_data = data;
        	    int i;
        
        	    while (index >= size) 
        	      size *= 2;
        	    data = new Element[size];
        	    for (i = 0; i < old_size; i++)
        	      data[i] = old_data[i];
        	    delete [] old_data;
        	  }
        	  data[index] = value;
        	}
        
        	// get the value at location index
        	template <class Element, int ArraySize>  
        	Element DynArray<Element, ArraySize>::getValue(int index)
        	{
        	  if (index < 0) {
        	    cerr << "Index " << index << " is negative" << endl;
        	    exit(1);
        	  }
        	  else if (index >= size) {
        	    cerr << "Index " << index << " is out of range" << endl;
        	    cerr << "The current size of the array is " << size << endl;
        	    exit(1);
        	  }
                  else
        	    return data[index];
        	}
        
        	template <class Element, int ArraySize>  
        	int DynArray<Element, ArraySize>::getSize() 
        	{
        	  return size;
        	}
        
        
    4. Here are two sample declarations for DynArray:
      DynArray<int, 15> data;
      DynArray<int> *data = new DynArray<int>();
      
    5. Here is a sample driver program for this dynamic array. Note that you have to include DynArray.h, which in turn includes the DynArray.cpp file. It is not possible to compile a C++ template because it does not have any instantiated types. Hence you must include the template code in each file where you use that template code.
      #include <iostream>
      #include "DynArray.h"
      using namespace std;
      
      int main() {
          DynArray<int, 15> data;
          int i;
      
          for (i = 0; i < 1000; i++) 
      	data.setValue(i, i+2);
      
          for (i = 0; i < 1000; i+= 50)
      	cout << i << ": " << data.getValue(i) << endl;
      
          cout << "size = " << data.getSize() << endl;
      }
      
  4. Compilation of Class Templates: You have to include the template code in each file that uses it, so the question arises as to why the linker does not complain about multiple instantiations of the same functions. There are two separate approaches that C++ compilers/linkers take to this problem:

    1. Borland model: Each file separately compiles the template code, and the linker then collapses any duplicates. For example, if a stack template was instantiated for ints in two separate files, then each file would compile a copy of the code instantiated for ints, and the linker would then collapse the duplicate copies into one copy. This means long compile times and big .o files, but the executable file is not bloated. This is the model used by GNU ld version 2.8 or later on an ELF system such as GNU/Linux or Solaris 2, or on Microsoft Windows, g++. The GNU documentation is not clear about how it handled template instantiation pre-2.8.

    2. CFront Model: Template instantiations are added to a repository as the individual object files are built, and then the linker includes the instantiations in this repository in the final executable. This approach is more efficient, since each instantiation is only compiled once, but it requires a separate repository and complicates the compiler/linker, because some project management is implied (e.g., if the last file that uses a template instantiation is deleted, then the instantiation should be removed from the repository). With the Borland approach, only the object files themselves have to be considered--no external information is required. You can get the g++ compiler to use this approach via the use of the -frepo option.