Modern C++ - Templates

Financed from the financial support ELTE won from the Higher Education Restructuring Fund of the Hungarian Government.

16. Templates

Independent concepts should be independently represented and should be combined only when needed. Otherwise unrelated concepts are bundled together or unnecessarry dependencies are created.

In modern programming languages generics provide a simple way to represent a wide range of general concepts parameterized by type(s) and a simple ways to combine them. In C++ generics are implemented as templates.

The standard library – which requires a greater degree of generality, flexibility and efficiency – is a good example of template collection.

Alternative implementation ideas

Suppose we want to implement a max function to select the greater value for many different types.

 1 int max( int a, int b)
 2 {
 3   if ( a > b )
 4     return a;
 5   else
 6     return b;
 7 }
 8 double max( double a, double b)
 9 {
10   if ( a > b )
11     return a;
12   else
13     return b;
14 }
15 ... and so on ...

How can we minimize the copy-paste effect? Just overloading many functions different only in the type of the parameter is not maintainable.

Preprocessor macro

One natural attempt is to use the preprocessor macro.

#define MAX(a,b)    ((a) > (b) ? (a) : (b))

The parentheses are necessary because of the possible context of the macro expansion. Still, macros have side effects:

x = MAX(a++,b);

After the expansion become:

x = ((a++) > (b) ? (a++) : (b))

so when a++ > b the increment of a will be executed twice.

Even when we avoid these issues, the macro is processed by the preprocessor, therefore it cannot communicate with the type system.

Templates

Ada designers described generic program units as: a restricted form of context-sensitive macro facility, and the designers of C++ claims: templates are clever kind of macros that obeys the scope, naming, and type rules of C++.

1 template <typename T>  // same as: <class T>
2 T max( T a, T b)
3 {
4   if ( a > b )
5     return a;
6   else
7     return b;
8 }

The template parameter can be a type, a const expression, or a pointer to an external object or function.

Instantiation, parameter deduction

Template is not a function, template is a pattern to generate a function The concrete function is the specialization created by automatic instantiation.

1 int i, j = 3, k = 4;
2 double x, y = 5.1, z = 3.14;
3 
4 i = max( j, k);  // instantiates int max(int,int) specialization
5 x = max( y, z);  // instantiates double max(double,double) specialization

The compiler deduces the parameter types, then creates a specific version. The compiler instantiates the specialization with concrete types. Parameter deduction happens during compilation time.

Templates must be placed into header files, since the compiler must read their source.

Type exquivalence:

Templates are the same type only if all type parameters are equivalent.

1 C<char,10>      c1;
2 C<int, 10>      c2; // different than c1
3 C<int, 25-15>   c3; // same type as of c2

Explicit specialization

1 int i = 3, j = 4, k;
2 double  x = 3.14, y = 4.14, z;
3 const int ci = 6;
4 
5 k = max( i, j);     // -> max(int, int)
6 z = max( x, y);     // -> max(double, double)
7 k = max( i, ci);    // -> max(int, int)
8 
9 z = max( i, x);     // -> error

In the last case the parameter deduction failes, since there is contradiction between the type of i and x. This causes a compile time error.

To fix the problem, we try introduce an other version of max with more parameters.

 1 template <class R, class T, class S>
 2 R max( T a, S b)
 3 {
 4   if ( a > b )
 5      return a;
 6   else
 7      return b;
 8 }
 9 z = max( i, x);     // error, no parameter deduction on return value
10                     // impossible to deduce R

Unfortunately, this does not work, since there is no parameter deduction on the return type, therefore R is not deducible. The information on R should be presented explicitly:

1 z = max<double>( i, x);  // ok, return type is double
2 z = max<int>( i, x);     // ok, return type is int
3 z = max<int,double,int>( i, x);   // ok, R, T, S is given explicitly

User specialization

In the earlier examples, the different versions of max used the same algorithm, only the parameters were different. There are cases, when the algorithm should be different for specific types.

 1 const char *s1 = "Hello";
 2 const char *s2 = "world";
 3 
 4 template <>    // user specialization
 5 const char *max<const char *>( const char *s1, const char *s2)
 6 {
 7   return  strcmp( s1, s2) < 0;
 8 }
 9 
10 std::cout << max( s1, s2) << std::endl;   // uses user specialization

The earlier templates would match for max(const char *, const char *), but the usage of operator< would be misleading to compare pointers. Therefore we define a user specialization, that matches for this case.

Template overloading

We can use all the templates introduced earlier together. The compiler will apply the most specialized version to instantiate.

 1 template <class T>
 2 T max( T a, T b)
 3 {
 4   if ( a > b )
 5     return a;
 6   else
 7     return b;
 8 }
 9 template <class R, class T, class S>
10 R max( T a, S b)
11 {
12   if ( a > b )
13     return a;
14   else
15     return b;
16 }
17 template <>    // user specialization
18 const char *max<const char *>( const char *s1, const char *s2)
19 {
20   return  strcmp( s1, s2) < 0;
21 }
22 
23 i = max(3,4);                            // line 1
24 x = max<double>(3.14, 4);                // line 9
25 const char *m = max( "hello", "world");  // line 17

Class templates

Class templates define patterns for classes.

 1 template <typename T>
 2 class Matrix
 3 {
 4 public:
 5   Matrix( int cols, int rows) : _t ( new T[cols * rows] ) {}
 6   Matrix( const Matrix& rhs);
 7   Matrix& operator=(const Matrix& rhs);
 8   ~Matrix();
 9   // ...
10 private:
11   T* _t;
12   // ..   
13 };

All of the memberfunctions of class templates are template functions. Therefore we have to put both the class template declaration and all memberfunction implementations into header files.

Template member functions have special syntax:

1 template <typename T>
2 Matrix<T>::Matrix( const Matrix& rhs) {  ... }
3 ^          ^             ^
4 |          |             |
5 class   constr name   inside Matrix namespace this is Matrix<T>

Since class template parameters regularly cannot be deduced from constructor parameters, objects from template classes must be explicitly specialized:

1 #include "matrix.h"
2 
3 int main()
4 {
5   matrix<int>  m(10,20);
6 
7   m.at(2,3) = 1;
8   cout << m(2,3) << endl;
9 }

Class template specialization

We can specialize class templates writing a completely new version for a specific template parameter. The specialization does not need to be follow the interface of the generic version (although this is not a good design decision).

 1 template <>
 2 class Matrix<bool>
 3 {
 4 public:
 5   // possibly different interface
 6   // ...
 7 private:
 8   // possibly completely different implementations
 9   // ...  
10 };
11 
12 void f()
13 {
14   Matrix<int>     mi(10,20);  // use generic version
15   Matrix<double>  md(20,40);  // use generic version
16   Matrix<bool>    mb(20,40);  // use bool specialization
17   // ...
18 }

The complete example

Here we provide the template version of the vector class we implemented in Chapter 15.

  1 // vector.h
  2 #ifndef VECTOR_H
  3 #define VECTOR_H
  4 
  5 #include <stdexcept>
  6 
  7 template <typename T>
  8 class Vector
  9 {
 10 public:
 11   Vector();     // constructor
 12 
 13   Vector(const Vector& rhs);            // copy constructor
 14   Vector& operator=(const Vector& rhs); // assignment operator
 15 
 16   ~Vector();    // destructor
 17 
 18   int     size() const;    // actual size 
 19 
 20   T& operator[](int i);        // unchecked access
 21   T  operator[](int i) const;  // unchecked access, const member
 22 
 23   void    push_back(T d);       // append to end
 24   void    pop_back();           // remove from end;
 25 private:
 26   int     _size;        // actual number of elements
 27   int     _capacity;    // buffer size
 28   T*      _ptr;         // pointer to buffer
 29 
 30   void copy(const Vector& rhs);   // private helper function
 31   void release();                 // private helper function
 32   void grow();                    // reallocate buffer
 33 };
 34 
 35 // the implementation is in the same header file
 36 template <typename T>
 37 Vector<T>::Vector()
 38 {
 39   _capacity = 4;
 40   _size = 0;
 41   _ptr = new T[_capacity];
 42 }
 43 template <typename T>
 44 Vector<T>::Vector(const Vector& rhs)
 45 {
 46   copy(rhs);
 47 }
 48 template <typename T>
 49 Vector<T>& Vector<T>::operator=(const Vector& rhs)
 50 {
 51   if ( this != &rhs )  // avoid x = x
 52   {
 53     release();
 54     copy(rhs);
 55   }
 56   return *this;  // for x = y = z
 57 }
 58 template <typename T>
 59 Vector<T>::~Vector()
 60 {
 61   release();
 62 }
 63 template <typename T>
 64 void Vector<T>::copy(const Vector& rhs)
 65 {
 66   _capacity = rhs._capacity;
 67   _size = rhs._size;
 68   _ptr = new T[_capacity];
 69 
 70   for (int i = 0; i < _size; ++i)
 71     _ptr[i] = rhs._ptr[i];
 72 }
 73 template <typename T>
 74 void Vector<T>::release()
 75 {
 76   delete [] _ptr;
 77 }
 78 template <typename T>
 79 void Vector<T>::grow()
 80 {
 81   T *_oldptr = _ptr;
 82   _capacity = 2 * _capacity;
 83   _ptr = new T[_capacity];
 84 
 85   for ( int i = 0; i < _size; ++i)
 86     _ptr[i] = _oldptr[i];
 87 
 88   delete [] _oldptr;
 89 }
 90 template <typename T>
 91 int Vector<T>::size() const
 92 {
 93   return _size;
 94 }
 95 template <typename T>
 96 T& Vector<T>::operator[](int i)
 97 {
 98   return _ptr[i];
 99 }
100 template <typename T>
101 T Vector<T>::operator[](int i) const
102 {
103   return _ptr[i];
104 }
105 template <typename T>
106 void Vector<T>::push_back(T d)
107 {
108   if ( _size == _capacity )
109     grow();
110 
111   _ptr[_size] = d;
112   ++_size;
113 }
114 template <typename T>
115 void Vector<T>::pop_back()
116 {
117   if ( 0 == _size )
118     throw std::out_of_range("vector empty");
119 
120   --_size;
121 }
122 #endif /* VECTOR_H */
 1 #include <iostream>
 2 #include <string>
 3 #include "vector.h"
 4 
 5 template <typename T>
 6 void print(const vec<T>& v, char *name)
 7 {
 8   std::cout << s << " = [ ";
 9   for (int i = 0; i < v.size(); ++i )
10     std::cout << v[i] << " ";
11   std::cout << "]" << std::endl;
12 }
13 int main()
14 {
15   Vector<int> x;  // declare and fill x
16   for (int i = 0; i < 10; ++i )
17     x.push_back(i);
18 
19   Vector<double> y;  // declare and fill y
20   for (int i = 0; i < 15; ++i )
21     y.push_back(i+20.5);
22 
23   std::string s;
24   vector<std::string> z; // declare and fill z
25   for (char ch = 'a'; ch < 'z'; ++ch )
26   {
27     s += ch;
28     z.push_back(s);
29   }
30   print(x,"x");
31   print(y,"y");
32   print(z,"z");
33 }