An introduction to C++ templates
Introduction
Templates are a very powerful – but often very confusing – mechanism within C++. However, approached in stages, templates can be readily understood (despite their heinous syntax).
The aim of this series of articles is to guide beginners through the syntax and semantics of the foundation concepts in C++ template programming.
Let's start simple and familiar…
Generic programming
With a strongly-typed language we may find ourselves having to implement a different version of a common function for each type we have to deal with:
This can be tedious, error prone and time-consuming (especially if there is a common bug). It also can lead to code bloat for library code when you want to support as many types as possible (even though good compilers may optimise away unreferenced code).
An alternative, using a C approach, is to define a macro and let the C pre-processor expand it. This is generally OK for base types, but what if the macro arguments are more complex, or we can get unwanted side effects?
Since the macro is evaluated by the pre-processor (before compilation) it is working purely on the source code text, which means:
- Parameters are not type-checked
- Parameters are not evaluated
The subtle problem with the shown macro is that the value of one of its arguments will be evaluated twice, once during the test and once when calculating the return value. Consider:
Template functions
Template functions extend the capabilities of the function-like macro to allow the compiler (rather than the pre-processor) to create new functions based on some 'scaffold' code written by the programmer.
Variables and arguments in the template code can be parameterised for different types, allowing the compiler to generate different variants on the function, as required.
Candidate functions for templates are those whose implementation remains invariant over a set of instances. That is, the algorithmic behaviour of the code is the same, independent of the type of objects the algorithm is processing.
The keyword template always begins a definition and declaration of a function template. The <> brackets contain a list of template parameters. The replaceable elements in the code – the template parameters – must be identified as such in the template parameter list.
The keyword typename tells the compiler that what follows is the name of a type (occasionally, you may see the keyword class in its place, but this can be confusing so I have avoided it in this article)
To use the template function you call it with the required type in angle-brackets. The compiler will generate a new function for each new type parameter you supply.
In many cases the template type can be deduced from the invocation of the function. For example min has two arguments and a return type all of the same type. In the call min(10,20) both arguments are integers, therefore the compiler can deduce that typename T maps onto int.
If it cannot deduce the type from the argument – e.g. min (10, 20.9) (and note, there are no conversion rules) then a compiler error message is generated. This can be resolved by forcing the type using explicit instantiation.
Under the hood
New code is generated for each new type that the compiler encounters. The compiler uses the template code to generate a new function with the template parameter replaced by the supplied type.
A template instance is only created once. If multiple calls to min with int or double arguments are encountered (or any type for that matter) then still only one set of object code is created per translation unit.
Multiple template parameters
If differing types are required, then multiple type parameters can be specified.
Unfortunately, in this form a return type cannot be deduced, so you have to add it explicitly as a template parameter. By making it the first parameter, the other parameters can be deduced.
Note, the programmer is responsible for specifying the return type, and an incorrect choice could lead to truncation (for example supplying an integer return type for two floating point parameters)
This is somewhat clumsy – particularly for library code – so a new mechanism was introduction for C++11.
The decltype keyword deduces the type of an expression. C++ is an expression-based language. All expressions have both a value (the result) and a type. The decltype operator simply returns the type, rather than the value.
We can use decltype to deduce the return type from functions, too. Here's a first attempt with our min function:
First thing to note: the expression inside the decltype isn't evaluated, so we must still have the expression inside the body of the function.
Second (and far more important): This code will not compile! decltype is trying to evaluate an expression based on the types of a and b, but they have not been declared yet.
To fix this we need to use trailing return type syntax, instead.
Putting the return type after the parameter list allows the parameters to be used to specify the return type. The keyword auto is required to tell the compiler the return type is specified after the parameters。
Problems with template functions
Whenever you use a template function then certain requirements will be placed on the argument type. If the type does not support the operation then the compiler will generate an error message (deciphering these message is a real art and very compiler-dependent). The error will normally be flagged in the template code not at the Point-of-Instantiation (PoI). This can make using templates sometime very frustrating.
In the above example, we are attempting to use our min template function with some abstract data type (class ADT). When we compile we get an error in the template function – type T must support operator<.
Remember, for a class type the expression (a < b) resolves to (a.operator<(b))
In this instance the compiler has no way of comparing two ADT objects, so get our code to work we must supply an overload for operator< on the class ADT that allows us to compare them (I'll leave that as an exercise to the reader!)
Function overloading and templates
Sometimes template expansion may generate either incorrect or inefficient versions.
The output from this example is just as likely to be 'world' as 'hello'. When comparing string literals the compiler deduces the template type as const char*; therefore the function is comparing pointer values, rather than strings. The answer you get depends on the order the compiler stores the string literals in the program.
Usefully, template functions (just like any other function) can be overloaded.
In the code above we have written a free function version of min that uses the C standard library functionstrcmp (which correctly identifies "hello" < "world").
When you call min the compiler is required to deduce the template parameters (which are, of course, of type const char*). This causes a problem: The template instantiation will be const char* min(const char*, const char*), which matches the non-template overload of the function The compiler will always prefer the non-template versions in such cases.
In the second case we are explicitly instantiating (and calling) the template function with const char*; there is no type deduction, so no ambiguity.
Summary
Template functions are a useful mechanism for implementing 'generic' algorithms – that is, those algorithms where the only difference is the types of the parameters being operated on.
However, the real power of templates starts when we combine template functions with classes.
So that's what we'll look at next time.
Template classes
Introduction
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.
Class templates
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> orMeasurement<double> (etc.). A common mistake is to attempt to declare a pointer to a Measurement class, that is:
Measurement* pMeas;
But, of course, this will fail to compile. The pointer type must have a template parameter, for example:
Measurement<int>* pIntMeas;
Measurement<double>* pDblMeas;
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).
Template constructors
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)
Summary
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.
Template inheritance
Introduction
Previously we looked at template class syntax and semantics. In this article we'll extend this to look at inheritance of template classes.
Inheriting from a template class
It is possible to inherit from a template class. All the usual rules for inheritance and polymorphism apply.
If we want the new, derived class to be generic it should also be a template class; and pass its template parameter along to the base class.
Notice that we 'pass-down' the template parameter from the derived class to the base class since there is no class Base, only class Base<T>. The same holds true if we wish to explicitly call a base class method (in this example, the call to Base<T>::set())
We can use this facility to build generic versions of the Class Adapter Pattern – using a derived template class to modify the interface of the base class.
Notice, in this case we use private inheritance. This hides the base class methods to any clients (but keeps them accessible to the derived class. Calls to the Adapter's methods are forwarded on to the base class. For more on the Adapter Pattern see this article.
Extending the derived class
The derived class may itself have template parameters. Please note there must be enough template parameters in the derived class to satisfy the requirements of the base class.
Specialising the base class
The derived class may also specialise the base class. In this case we are creating an explicit instance of the base class for all versions of the derived class. That is, all derived class instances, regardless of the type used to instantiate them, will have a base class with a data object of type int.
Deriving from a non-template base class
There is no requirement that your base class be a template. It is quite possible to have a template class inherit from a 'normal' class.
This mechanism is recommended if your template class has a lot of non-template attributes and operations. Instead of putting them in the template class, put them into a non-template base class. This can stop a proliferation of non-template member function code being generated for each template instantiation.
Parameterised inheritance
So far, we have been inheriting explicitly from another (base) class, but there is no reason why we can't inherit from a template type instead – with the (reasonably obvious) requirement that the template parameter must be a class type. This mechanism allows some neat facilities to be implemented.
In the example below we have built a template class, Named. The class allows a name (string) to be prepended to any class, with the proviso that the base class (the template parameter) supports the member function display().
In this example we have constructed a normal class, Waypoint, which supports the display() method (presumably, this displays the current coordinates of the Waypoint; but who knows…).
When we create a Named object, we give it the type of the object we want to 'decorate' with a name (in this case, a Waypoint). Calling display() on a Named object will cause it to output the object's name, before calling display on its base class – in our example Waypoint::display().
(NOTE: I'm quite deliberately ignoring the 'Elephant in the Room' with this code – how to generically construct a base class object with a non-default constructor. I'll revisit this example again in the future when we look at Variadic Templates.)
Summary
This time we've covered most of the basic inheritance patterns for templates. Next time we'll have a look at a practical application for templates in your application code.
Templates and polymorphism
Introduction
Template functions and classes tend to cause consternation amongst programmers. The conversation tends to go something like this:
- I understand the syntax of templates (although it's ugly)
- I get the idea of replacing function-like macros with template functions
- I can see the application of template classes for containers
- Most containers and generic functions are library code
- I don't write libraries
- What's the point of me using templates?
In this article we're going to look at an application of templates beyond writing library code – replacing run-time polymorphism (interfaces) with compile-time polymorphism. This idea is known as a Policy. The idea is reminiscent of the Strategy Pattern, but uses templates rather than interfaces.
I'm going to assume you already have an understanding of basic template syntax and semantics. If you're a little rusty on these things look here and here for a refresher.
Setting the scene
In a multi-threaded environment it's typical for threads to communicate asynchronously using First-In-First-Out message queues. (Don't fret at this point about multi-threaded design; it's the construction of the queue we're going to worry about here, not how it would be used)
Because of the potential for race conditions within the queue (both threads trying to modify the queue 'simultaneously' – see here for a more detailed description) we should protect it with a mutual exclusion mechanism. For the purposes of this article we'll deliberately keep the queue code simplistic (in other words: don't try and use this queue in your production code!)
Here's our first pass at the queue code:
For this example we've written the code as an adapter for the Standard Library queue class (using a variation of the Class Adapter Pattern). You can explore the semantics of template inheritance here if you're not familiar with it.
The flexibility of our design is limited by the fact we have hard-coded a concrete Mutex object into theMessageQueue. This could be a serious penalty if:
- We want to port to a different operating system (perhaps with more efficient mutual exclusion mechanisms)
- We want to use the MessageQueue in non-threaded code, where we don't want (or require) the overhead of locking/unlocking a Mutex object.
Let's look at a couple of solutions that can improve our design.
An interface solution
Firstly, we want to de-couple our MessageQueue class from any particular Mutex implementation. The usual way to do that is via an Interface.
The MessageQueue has an association to the Interface; and we can substitute classes that realise the interface.
Here's the code:
If you're familiar with using Interfaces the code above should be very familiar. The association between theMessageQueue and the Interface is implemented as a pointer. The binding between a MessageQueue object and a (concrete) Mutex class is done during the construction of the MessageQueue.
Now we have a mechanism for dealing with our above problems.
If we move to a new OS we can substitute a new class that realises the I_Mutex interface. (You might want to consider using the Abstract Factory Pattern for this code to eradicate a host of conditional compilation).
For non-threaded code we may implement a 'Null Mutex' – that is, a concrete implementation with empty methods. These calls will typically be optimised away by the compiler to leave a sequential version of theMessageQueue, with no lock/unlock overheads.
However this flexibility comes at a cost. The run-time lookup for virtual functions requires additional code and data. Each dynamic polymorphic class requires a virtual table (v-table), and each object of that type av-table pointer (vtptr). To call the polymorphic function the run-time system requires indexing into the v-tablevia the object's vtptr to actually call the function. In certain environments this can be up to twice as slow as a normal function call.
The template policy
One of the benefits of Interfaces is that it allows substitution of implementation even at run-time. However, you have to ask: are you likely to replace your operating system during program run-time? Reasonably, you're more likely to select the OS at compile time (like the example above) and not change it. Thus we are paying the price for flexibility we aren't going to use.
Step up, templates.
One of the characteristics of templates is that they impose certain requirements on the types that can be used to instantiate the template. It could be particular attributes, but it's more likely to be particular behaviours (for example, the need for types to support operator< if they want to be sorted in a container).
Let's redefine our MessageQueue class using a template parameter to enforce a Policy: whatever type is used to instantiate the template must support the methods lock() and unlock(). The MessageQueue class is referred to as a host class
Please note, this is the only requirement we are demanding. There is no need to support any other methods, or realise an Interface; in fact, we're even being pretty lenient on the parameters and return types of the functions.
We define our 'policy' when we instantiate the MessageQueue class by specifying the type of mutex implementation we want.
Notice in the above code the VxWorksMutex supports a far richer interface than required by ourMessageQueue. This does not affect its usage in our code. In this example, the default timeout parameter is used for the calls to lock(); and the error return codes are ignored.
Since there are no interfaces or virtual functions in the template code all calls are statically-bound at compile time, meaning there is no overhead of virtual tables or v-table pointers.
Summary
Polymorphism is one of the cornerstones of building extensible, flexible software in C++. Dynamic polymorphism, via substitution, virtual functions and Interfaces provide a mechanism to enact this. However, that flexibility comes at a run-time cost.
Templates offer a similar flexibility – and in many ways even more flexibility – without the run-time overheads of dynamic polymorphism. The trade-off now is the fact that the flexibility is fixed at compile-time – what we might call Compile-Time polymorphism.
The purpose of Interfaces (and Policies) is to provide architectural flexibility in our systems by defining clear code 'seams' – points in the architecture where elements can be removed and replaced with minimal effort. Many of those seams are related to hardware or other system factors and will never be reconfigured during system operation. In such cases it makes sense to replace Interfaces with template policies to help improve the run-time performance of the code.
Template member functions
Introduction
Previously we've looked at template functions and we've looked at template classes. This time, let's look at what happens when you combine them.
Template member functions
A non-template class can have template member functions, if required.
Notice the syntax. Unlike a member function for a template class, a template member function is just like a free template function but scoped to its containing class. In this case the compiler generates a member function for each (unique) template type. As before, the compiler will favour a non-template overload of a member function.
Template functions on template classes
What about if our class is itself a template? A template class will typically have member functions defined in terms of its own template parameter, but may equally have member functions that are themselves template functions. In this case the member function's template parameter is different to the owning class'.
The syntax for specifying the template member function is rather obtuse (to say the least):
The containing class' template declaration must be 'outer' one; func is a template function (typename U) that belongs to the class Utility<T> (typename T)
Notice that the template function must have a different template parameter identifier to its containing class (even if the same type is being used to instantiate both the class and the function).
Template constructors
A common application of template member functions on template classes is constructors; particularly if inheritance is involved. (Have a look here for more on template inheritance)
Let's revisit a previous example.
Above is an example of parameterised inheritance. In the earlier article I (deliberately) ignored the construction of the base class objects. The problem we face is that we cannot determine the structure of the base class constructor – each base class constructor will have its own number of parameters, with different types – so how can we write a Named constructor that satisfies any potential base class?
Let's split this problem into two: parameters of different types; and different numbers of parameters
We can deal with different parameter types by making the Named class constructor a template member function.
OK… deep breath… let's wade through this mire of syntax and work out what's happening.
The constructor for Named has three parameters: a string literal (const char*) and two template parameter arguments. Notice these parameters are passed as r-value references (Arg&&). In this situation Scott Meyers refers to these as Universal References – meaning "a reference that can bind to anything".
In the body of the constructor, notice the use of the template function std::forward. This function ensures that the types of the parameters are forwarded on without changing their types – that is, l-values remain l-values, r-values remain r-values.
(I'm deliberately glossing over the details of these mechanisms here as they're not the focus of this article. For a detailed description I highly recommend reading Meyers' excellent article "Universal References in C++11"
Now when we construct a Named object, its constructor (function) can deduce the types of the supplied arguments and pass them on to the (template-parameter) base class.
So far, so good, but our Named constructor is still limited: It will only work for base classes that have exactly two parameters. If we try and use a class with three parameters we get a problem:
Our Lamp class is a candidate for being the Named base class, since it supports the display() method. However, its constructor requires three parameters – and our template constructor can only supply two.
The short-term fix is to overload the Named constructor to take three parameters:
This works; but it means we'll have to overload the constructor for zero parameters, one parameter, two, three, four, etc. This can quickly become onerous, so in the next article we'll have a look at a new mechanism in C++11 designed to make this more flexible – Variadic Templates.
Summary
Template member functions allow us to parameterise functions independently of the class they belong to. They can be added to both template and non-template classes.
Template member functions follow all the usual rules of template functions – they can be overloaded (both by template versions and non-template versions) and they may be overridden by derived classes.
Variadic templates
Introduction
In this article we're going to look at a new feature of templates in C++11 – the concept of the variadic template.
Variadic templates allow us to create functions and classes, not only with generic types, but also a variablenumber of generic types.
If you haven't been following along with the template articles, I'd suggest you read this article first before continuing.
The problem (so far)
Previously, we used a template constructor on a template class to allow us to construct a base class with a variable number of constructor parameters of different types. Here's where we got to:
Our Named class allows us to prepend a name string to any class that implements the display() method. In order to deal with (template parameter) classes that have non-default constructors we have provided theNamed class with a template constructor function. This function has been overloaded to handle base classes with two and three (template) parameters. Given the classes below, our Named class works just fine:
The code as it currently stands means we'll have to overload the Named constructor every time we use a base class with a different number of parameters. This will quickly become onerous.
We need a more elegant solution.
Variadic templates
A variadic template is a template which can take an arbitrary number of template arguments, of any type. Both classes and functions can be variadic.
Below, I've replaced our overloaded constructors with a variadic version.
The … operator has two roles. When it occurs to the left of the name of a parameter, it declares a parameter pack. By using the parameter pack, user can bind zero or more arguments to the variadic template parameters. Parameter packs can also be used for non-type parameters.
By contrast, when the … operator occurs to the right of a template or function call argument, it unpacks the parameter packs into separate arguments.
In practice, the use of the … operator in the code means that the whole expression that precedes the …operator will be repeated for every next argument unpacked from the argument pack, and all these expressions will be separated by a comma.
That is,
Now, the compiler will use the variadic template to construct the appropriate constructor for the type of base class being instantiated.
Here the compiler deduces not only the types of the template parameters from the call, but also the number of template parameters required. This is used to construct a version of the template constructor with the appropriate number of template parameters.
Summary
Using variadic templates, the compiler is doing the task we were performing earlier by creating the appropriate template overloads for our class. However, unlike us, the compiler doesn't have to provide overloads for an arbitrary number of template parameters, just in case – it can create only the overloads it needs for this system.
This mechanism is a much more elegant way of creating generic objects, particularly in library code when it cannot be known a priori the nature of the classes that will be used as template parameters.
Templates of templates
Introduction
In this brief article we'll have a look at the topic of building template classes whose parameters are themselves templates.
I'm assuming you're reasonably familiar with template classes. If not, here's a quick introduction.
The problem
Let's start with a case study. Suppose we want to build a simple hash table class. Our class should be generic for different types of keys and values*. Obviously, it should be a template class.
We might want to add more flexibility to our design by allowing the client to choose the type of container class used by the SimpleHashTable to store its keys and values (for example, for performance reasons we may want to use a fixed-size container)
We need to add the container class as a template parameter. However, a container class is typically a template class itself.
Let's modify the SimpleHashTable to allow the container type to be specified:
We must tell the compiler that the template parameter Container_Type is itself a template class. Notice that the template parameter for the template class must not match any of the other identifiers in the template list.
The Container_Type parameter is used to instantiate a new container within the SimpleHashTable, whose template parameter is one of the template parameters supplied to the SimpleHashTable. In other words, there is a 'cascading' of template parameters within the class: a parameter supplied to the owning template is used to instantiate a nested template class within.
Let's have a look at what gets created when we instantiate our SimpleHashtable class:
As with all template parameters, template-template parameters can be defaulted (the usual rules for defaults apply). Note, however, that the template default must be a template class.
Summary
Once we start building template classes it becomes natural to incorporate them into other, more complex, template classes. The aim is to build our generic code with as much flexibility as possible.
Template specialisation
Introduction
Welcome back to the wonderful world of templates.
So far, we have looked at what are known as base templates. In this article we're going to look at one of the more confusing aspects of templates – specialisation. The choice of the word specialisation is unfortunate, as many confuse it with inheritance and sub-typing; in this case specialised means "more specific".
Template specialisation comes in two forms:
- Explict specialisation, where a unique version of the template is defined for a specific type
- Partial specialisation, where a template is defined that acts on a qualified range of types (for example, pointers-to-type)
To add to the confusion template functions and template classes behave in different ways:
- Template functions may be explicitly specialised, but not partially specialised. Template functions may be overloaded, though.
- Template classes cannot be overloaded; but they can be both explicitly- and partially-specialised.
To help overcome some of this confusion, think how non-template functions and classes work:
- Functions can be overloaded, but not inherited
- Classes may be inherited, but not overloaded
Template functions and classes attempt to follow similar semantics. But let's have a look at them in more detail.
Overloading template functions
Back in the first article in this series we said that templates can be overloaded with non-template versions.
The compiler will always favour the non-template version of the function.
It is also possible to overload the template function with another template function.
With no non-template overload, the compiler chooses the template function that is most specialised – that is, the template function that best fits the parameters it deduces from the call.
In the first call, the compiler deduces the parameter type is int* (the declared type of p1 and p2) and so instantiates the template that matches best – T* min(T* a, T* b)
In the second call, the compiler deduces the type of T as int, and so instantiates the less-specialised template – T min(T a, T b).
Explicitly specialised template functions
It is also possible to explicitly specialise a template function. An explicitly specialised template function is when the function is declared for a specific type.
The above code gives ostensibly the same results as the non-template overload. However, because of the (arcane) rules the compiler uses to determine which template to instantiate you may not always get the result you anticipated.
I recommend that you ALWAYS prefer non-template overloads to template function specialisation.
(The reasons are beyond the scope of this article; and Herb Sutter has written an excellent description of the problem here.)
Template functions may be overloaded for different signatures and explicit specialisations which gives theillusion of function partial specialisation. However, because of subtleties in the way the compiler decides which template function to instantiate, overloading template functions with other template functions is best avoided.(The reasons are beyond the scope of this article; and Herb Sutter has written an excellent description of the problem here.)
Explicitly specialised template classes
Let's switch our attention to template classes. As I said in the introduction, with classes we can provide both explicitly specialised and partially-specialised versions of the template class.
In this example we have a class MinFinder, with a single method, get(). We can create instances of the template class for different types but in the case of strings the behaviour we get is not (necessarily) what we might want.
We can explicitly specialise the MinFinder class, creating a version that works specifically for const char*objects.
Notice in this case the compiler is not doing any type deduction – we are explicitly telling the compiler which template class to instantiate. This is a bit clumsy; we'll look into that shortly.
Partially specialised template classes
In the above example we have specialised for a particular type (const char*). However, we may want different behaviour for all pointer types – for example, comparing the referents of the pointers, rather than the pointers themselves. Unlike template functions we can't overload template classes. It would be impracticable to provide explicit specialisations for every pointer type, so C++ allows us to provide a partial specialisation.
Now we have defined:
- A base template, which works for everything; except…
- A partial specialisation, which will be favoured for all pointers-to-type; except…
- An explicit specialisation, which works specifically for const char* types.
The compiler will choose the appropriate template to instantiate based on the type of the template parameter it is instantiated with. Partial specialisations may be created for:
- Pointers
- References (r-value and l-value)
- const objects
- …and (valid) combinations of the above
Note that, if an explicit specialisation exists the compiler will prefer it to the partial specialisation. The compiler will always prefer the 'most specialised' (read: 'most specific') template. This extends for non-template classes – which, like template functions, will always be preferred to the equivalent template version
Hiding the template specialisations
At this point we have the ability to provide unique behaviours through partial- and explicitly-specialised template classes; but we must explicitly create the class we want to use.
To improve the usability of our code we need to combine template functions and template classes:
- Use the compiler's ability to deduce template types for template functions
- Use the ability to partially specialise template classes to get different beahviours for different (groups of) types.
The mechanism is to wrap the instantiation of a template class inside a template function:
The compiler deduces the template parameters from the function call; this is then used to create the appropriate (partially) specialised class, which does the work for us.
Summary
Template specialisation is a powerful way of building expressive library code, by allowing us to define different beahviours for different 'categories' of types. The real strength of this is allowing us to extend libraries after-the-fact, for types that may have not been considered during the library's conception.
Template functions may be overloaded for different signatures and explicit specialisations which gives theillusion of function partial specialisation. However, because of subtleties in the way the compiler decides which template function to instantiate, overloading template functions with other template functions is best avoided.
Template classes can be both explicitly and partially specialised, but must be explicitly instantiated. This presents a possible limitation for library code, but can be circumvented by combining template partial specialisations with template function type deduction.
In the final article of this series we'll have a look at the problem of communicating type information between different templates, using trait template classes.
没有评论:
发表评论