Last time we looked at template functions, which introduced the concept of generic programming in C++.
This time let’s extend the idea of generic programming to classes.
Let’s start with a simple case study. Our design may suggest the use of ‘measurement’ types – for example, readings from sensors. There may be different measurement types in the design, depending on the source of the data. The operations on these types (e.g. initialising, setting, reading, etc.) are common, the only difference being the stored data type. Ideally we’d like to define a common interface, but due to the different data types we cannot use pure virtual functions.
Enter the class template. A class template is a prescription for creating a class in which one or more types or values are parameterised.
The Measurement class template has a single template parameter; but like function templates it can have many parameters.
On instantiation, objects are created for both int and double types.
Notice, it is not possible for the compiler to deduce the template parameter type from the class instantiation, so you must explicitly define it.
As with function templates the compiler generates multiple data and function declarations relating to the types requested in class template declarations.
Note, there is no longer a simple class Measurement; only objects of type Measurement<int> or Measurement<double> (etc.). A common mistake is to attempt to declare a pointer to a Measurement class, that is:
But, of course, this will fail to compile. The pointer type must have a template parameter, for example:
Note these two pointer types are NOT the same.
Structuring template classes
A member function of a class template can be defined within or outside the class template definition. In both cases the function definition must be in the same file as the class declaration; since the function definitions need to be seen by the compiler at template instantiation.
Note the syntax. Each member function must be declared as a template (with the appropriate template parameters); the scope of the function is now Measurement<T>::, not Measurement::
Remember that the functions are now no longer inline by default (although they can be explicitly declared as so).
It’s generally considered good practice that, if you supply any constructor, you should also supply a default constructor.
With the template we have a problem – what should the default value be? You could use 0 (zero) but that is really an integer; and probably won’t work with all types.
We know that we can call a default constructor of a class type by using the class name with empty parentheses (for example, DataType()), but what about built-in types? If you use the syntax of an explicit constructor call without arguments, fundamental types are initialised to zero (or its equivalent). This is basically a default constructor for the basic types.
We can use this technique in our template code to create default constructors. For any generic type, T, we can explicitly call its default constructor. Our Measurement template can now be constructed with any type that has a default constructor.
Template type and non-type parameters
Parameters can also be an instance of a template type. In this example ival must be of type T. So if T is type int, then ival must be an integer value.
When we create an instance of this template class we have to supply two parameters – the type and an instance of that type.
Notice the Measurement constructor in the above code. It uses the template type parameter, ival to (default) initialise the class. The template type parameter can be thought of a constant, that all instances of the class possess (compare this to a static const class member)
In our example, if you don’t provide a parameter to the Measurement class’ constructor, the template parameter will be used as the default. However, if you do supply a value it will override the default (the template type parameter).
The standard says you cannot supply a literal double or char* as a template type parameter. Some (older) compilers don’t pick this up!
A non-type template parameter is a template parameter which isn’t a type or an instance of a type. In other words, it’s a plain-old value. However, there are some fairly stringent exceptions to what sort of value it can be.
A non-type template parameter must be a constant-expression – that is, an expression that can be determined at compile time (and does not change). A non-constant expression (that is, a variable) cannot be used as a template parameter since it could change at run-time, which would require the template to be regenerated; something that cannot be done.
A non-type template parameter can be:
- An integer value
- An enumeration
- The address of a function or a static object
- The address of a member function or member variable
A non-type template parameter cannot be:
- A floating point type
- A string literal
It’s worth noticing – and being wary of the fact – that each new combination of type and non-type parameter instance would cause the compiler to generate a new class. That is, a Measurement<int, 10> is a different type to a Measurement<int, 100>.
Template parameter defaults
Like function arguments, a template parameter can be given a default. This must be done in the class declaration.
If a template type (or integral parameter) is not specified when the template is instantiated the default value is used. Note you must always use the <> notation when instantiating the template class, even when using all the default values.
Under the hood
Template Instantiation is a fairly expensive process for most compilers, particularly with respect to memory consumption. Overhead varies hugely from compiler to compiler.
Greedy instantiation will create code for each instance of the template encountered, and rely on the linker to optimise the copies away. Generally this model works well, although the compiler may waste time optimising template instances that will later be discarded; and there is a small possibility the linker will retain a un-optimised implementation over an optimised one.
On-demand instantiation (a slightly misleading term) is where all declarations and definitions are generated at the first point-of-instantiation (PoI). Combined with Greed instantiation, templates in older compilers got a bad reputation for ‘code bloat’.
Modern compilers use a technique call Lazy instantiation. The declarations for all member functions are generated when an object of the template is instantiated; however the function definitions (the object code) are only generated if a member function is actually called. This means object code can, in some cases, be smaller than the equivalent non-template code.
Lazy instantiation becomes especially important for being able to write ‘rich’ library APIs without penalising the uses of the template code.
There are a couple of exceptions with lazy instantiation, where definitions are always generated:
- If the class contains an anonymous union
- Inline functions
- Virtual functions
A more recent development is known as Queried instantiation. This technique stores a database of template instantiations, independent from the application’s object files. The database is queried to see if a (current) template instantiation already exists. If it does the instantiation is used; if not it is generated. This technique requires the compiler to maintain the template database; and breaks the traditional separation between object files whilst compiling.
Finally, it’s worth noting the additional code overheads associated with the generated template instance code; for example
- Virtual (function) tables for each instantiated class (if it has virtual functions)
- Exception specifications (if exceptions are enabled)
Template classes extend the basic type mechanism to allow us to build generic types – abstractions of services (responsibility) whose data components can be defined by the programmer at compile time. This mechanism allows us to build very flexible libraries.
This article has covered the fundamentals of template classes. In upcoming articles we’ll explore more sophisticated template behaviour and also look at practical applications for template code.
For more information on templates, and other aspects of C++ programming, have a look at our C++ training courses: