Sans Pareil Technologies, Inc.

Key To Your Business

Lesson 12 - Templates

Templates are the basis for generic programming in C++. Templates are a way of making your classes more abstract by letting you define the behaviour of the class without actually knowing what datatype will be handled by the operations of the class. Templates are more focused on the algorithmic thought rather than the specific nuances of a single datatype. Templates can be applied either at a function level (algorithm), or at class level (data abstraction).

As a strongly-typed language, C++ requires all variables to have a specific type, either explicitly declared by the programmer or deduced by the compiler. However, many data structures and algorithms look the same no matter what type they are operating on. Templates enable you to define the operations of a class or function, and let the user specify what concrete types those operations should work on.

Defining Templates

A template is a construct that generates an ordinary type or function at compile time based on arguments the user supplies for the template parameters.

template <typename T>
T minimum( const T& lhs, const T& rhs )  
{  
  return lhs < rhs ? lhs : rhs;  
}

The above code describes a template for a generic function with a single type parameter T, whose return value and call parameters (lhs and rhs) are all of this type. You can name a type parameter anything you like, but by convention single upper case letters are most commonly used. T is a template parameter; the typename (or class) keyword says that this parameter is a placeholder for a type. When the function is called, the compiler will replace every instance of T with the concrete type argument that is either specified by the user or deduced by the compiler. The process in which the compiler generates a class or function from a template is referred to as template instantiation; minimum<int> is an instantiation of the template minimum<T>.

const int a = 1234;  
const int b = 2341;  
const int i = minimum<int>( a, b );
// or more concisely since the compiler can deduce the type of T from the arguments a and b
const int j = minimum(a, b);

When the compiler encounters that last statement, it generates a new function in which every occurrence of T in the template is replaced with int

int minimum( const int& lhs, const int& rhs )  
{  
  return lhs < rhs ? lhs : rhs;  
}

Type parameters

In the minimum template above, note that the type parameter T is not qualified in any way until it is used in the function call parameters, where the const and reference qualifiers are added.

There is no practical limit to the number of type parameters. Separate multiple parameters by commas:

template <typename T, typename U, typename V> class Foo{};

The keyword class is equivalent to typename in this context. You can express the previous example as:

template <class T, class U, class V> class Foo{};

You can use the ellipses operator () to define a template that takes an arbitrary number of zero or more type parameters:

template<typename... Arguments> class vtclass;

vtclass<> vtinstance1;  
vtclass<int> vtinstance2;  
vtclass<float, bool> vtinstance3;

Any built-in or user-defined type can be used as a type argument. For example, you can use std::vector in the Standard Library to store ints, doubles, strings, MyClass, const MyClass*, MyClass&. The primary restriction when using templates is that a type argument must support any operations that are applied to the type parameters. For example, if we call minimum using MyClass as in this example:

struct MyClass  
{  
  int num;  
  std::string description;  
};  
  
int main()  
{      
  const MyClass mc1{1, "hello"};  
  const MyClass mc2{2, "goodbye"};  
  auto result = minimum( mc1, mc2 ); // Error!
}

A compiler error will be generated because MyClass does not provide an overload for the < operator.

There is no inherent requirement that the type arguments for any particular template all belong to the same object hierarchy, although you can define a template that enforces such a restriction. You can combine object-oriented techniques with templates; for example, you can store a Derived* in a std::vector<Base*>. Note that the arguments must be pointers.

std::vector<MyClass*> vec;  
MyDerived d{3, "back again", time(0)};  
vec.push_back(&d);  
  
// or more realistically:  
std::vector<shared_ptr<MyClass>> vec2;  
vec2.emplace_back(std::make_shared<MyDerived>());

The basic requirements that std::vector and other standard library containers impose on elements of T is that T be copy-assignable and copy-constructible.

Non-type parameters

Unlike generic types in other languages such as C# and Java, C++ templates support non-type parameters, also called value parameters. For example, you can provide a constant integral value to specify the length of an array, as with this example that is similar to the std::array class in the Standard Library:

template<typename T, std::size_t L>  
class MyArray  
{  
  T arr[L];  
public:  
  MyArray() { ... }  
};

Note the syntax in the template declaration. The std::size_t value is passed in as a template argument at compile time and must be constant.

Other kinds of values including pointers and references can be passed in as non-type parameters. For example, you can pass in a pointer to a function or function object to customise some operation inside the template code.

Templates as template parameters

A template can be a template parameter. In this example, MyClass2 has two template parameters: a typename parameter T and a template parameter Arr:

template<typename T, template<typename U, int I> class Arr>  
class MyClass2  
{  
  T t; //OK  
  Arr<T, 10> a;  
  U u; //Error. U not in scope  
};

Since the Arr parameter itself has no body, its parameter names are not needed. In fact, it is an error to refer to Arr's typename or class parameter names from within the body of MyClass2. Arr's type parameter names can be omitted, as shown in this example:

template<typename T, template<typename, int> class Arr>  
class MyClass2  
{  
  T t; //OK  
  Arr<T, 10> a;  
};

Default template arguments

Class and function templates can have default arguments. When a template has a default argument you can leave it unspecified when you use it. For example, the std::vector template has a default argument for the allocator:

template <class T, class Allocator = allocator<T>> class vector;

In most cases the default std::allocator class is acceptable, so you use a vector like so:

``c++ vector myInts; ```

But if necessary you can specify a custom allocator

vector<int, MyAllocator> ints;

For multiple template arguments, all arguments after the first default argument must have default arguments.

When using a template whose parameters are all defaulted, use empty angle brackets:

template<typename A = int, typename B = double>  
class Bar  
{  
  //...  
};  
...  
int main()  
{  
  Bar<> bar; // use all default type arguments  
}

Template specialisation

In some cases, it is not possible or desirable for a template to define exactly the same code for any type. For example, you might wish to define a code path to be executed only if the type argument is a pointer, or a std::wstring, or a type derived from a particular base class. In such cases you can define a specialisation of the template for that particular type. When a user instantiates the template with that type, the compiler uses the specialisation to generate the class, and for all other types, the compiler chooses the more general template. Specialisations in which all parameters are specialised are complete specialisations. If only some of the parameters are specialised, it is called a partial specialisation.

template <typename K, typename V>  
class MyMap{/*...*/};  
  
// partial specialisation for string keys  
template<typename V>  
class MyMap<std::string, V> {/*...*/};  
...  
MyMap<int, MyClass> classes; // uses original template  
MyMap<std::string, MyClass> classes2; // uses the partial specialisation 

A template can have any number of specialisations as long as each specialised type parameter is unique. Only class templates may be partially specialised. All complete and partial specialisations of a template must be declared in the same namespace as the original template.

Usually when writing code it is easiest to precede from concrete to abstract; therefore, it is easier to write a class for a specific datatype and then proceed to a templated generic class.

Compile Time Polymorphism

Templates support compile-time polymorphism, also termed implicit interface. Unlike runtime polymorphism that requires inheritance and virtual functions, compile time polymorphism using templates only require that a valid template specialisation can be generated by the compiler for input types.

Advantages

  • Compile-time interfaces are much more granular than run-time ones. You can use only the requirements of a single function, or a set of functions, as you call them. You don't have to always do the whole interface. The requirements are only and exactly what you need.
  • Implicit interfaces are much easier to compose and multiply "inherit" than run-time interfaces, and don't impose any kind of binary restrictions - for example, POD classes can use implicit interfaces. There is no need for virtual inheritance or other heavyweight techniques with implicit interfaces.
  • The compiler can do way more optimisations for compile-time interfaces. In addition, the extra type safety makes for safer code.
  • It is impossible to do value typing for run-time interfaces, because you do not know the size or alignment of the final object. This means that any case which needs/benefits from value typing gains big benefits from templates.
  • Generally produces faster code than runtime inheritance.
  • Run-time inheritance is way less flexible (due to tight binding of code)

Disadvantages

  • Templates are difficult to compile and use, and they can be challenging (at times) porting between compilers. Heavy use of templates can lead to very large compilation times, and requirement for huge amount of free memory.
  • Templates cannot be loaded at run-time (obviously), so they have limits in expressing dynamic data structures, for example.
  • Run-time inheritance can express some data structures far more easily since the final type is only decided at runtime. Also you can export run-time polymorphic types across C boundaries.