2014年9月11日星期四

C++ Templates series

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:

image

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?

image

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:

image

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.

image

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.

image

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.

image

Multiple template parameters

If differing types are required, then multiple type parameters can be specified.

image

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.

image

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:

image

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.

image

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.

image

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.

image

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.

image

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.

image

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.

image

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.

image

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.

image

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.

image

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.

image

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.

image

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

image

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.

image

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.

image

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.

image

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.

image

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.

image

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.

image

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.

image

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().

image

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)

image

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:

image

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.

image

The MessageQueue has an association to the Interface; and we can substitute classes that realise the interface.

Here's the code:

image

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).

image

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.

image

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.

image

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

image

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.

image

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.

image

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):

image

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.

image

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.

image

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.

image

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:

image

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:

image

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:

image

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:

image

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.

image

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.

image

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,

image

Now, the compiler will use the variadic template to construct the appropriate constructor for the type of base class being instantiated.

image

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.

image

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.

image

Let's modify the SimpleHashTable to allow the container type to be specified:

image

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:

image

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.

image

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.

image

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.

image

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.

image

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.

image

We can explicitly specialise the MinFinder class, creating a version that works specifically for const char*objects.

image

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.

image

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:

image

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.

image

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.

没有评论:

发表评论