In this final article we’ll have a look at the issue of communicating template type information between different template instantiations, and have a look at the Traits mechanism as a method of solving these issues.
The problem with generic algorithms
If we’re writing generic code we may want to write a function (here called doStuff() – hey, I wasn’t feeling overly creative!) that can operate on containers of data.
The problem with our doStuff() is that it cannot know what type of elements are stored in the SeqContainer class. In this case we have made it an int, which (of course) only works for containers of ints (although other built-in types may be promoted/converted)
We could supply the type as a template parameter, but since the compiler cannot deduce the type from the call, we would have to ensure that the type of the objects in the container was the same as the type supplied to the algorithm. This is clumsy and error-prone.
A better solution would be to add type information about the elements inside the container. By exposing this information the algorithm can get information about the container’s contents.
The doStuff() algorithm can now use the public type definitions for the type of the object in the container. Note, however, this only works for containers providing these definitions; so our design is still a little limited and not very extensible (particularly for library code which may have to work for not-yet-created containers).
A trait class provides a generic set of type information definitions that algorithms may rely on. The actual type definitions are provided by the actual (container) classes. The traits class maps the generic request onto a specific definition at compile time.
The (set of) traits classes act as a database of type definitions, indexed by (container) type.
When you supply a container type to the algorithm it can use this information to look up the type information it needs for its particular container.
Although this adds a layer of (compile-time) complexity it adds a lot of flexibility to the design.
Below, the ContainerTraits class provides the mapping between the algorithm and the actual type definitions in the container.
The algorithm now relies on the definitions from the ContainerTraits template class. At compile time the following template instantiations occur:
- An instance of the SeqContainer template, with T => int
- An instance of the ContainerTraits template, with Container => SeqContainer
The typedef ContainerTraits<Container>::value_type is resolved as follows:
=> typedef T value_type
=> typedef int value_type
Notice we have to use the typename keyword in the ContainerTraits typedefs, otherwise the compiler cannot tell whether what is being specified is a type or an object (instance).
Supporting classes without type information
The real strength of traits classes is that we can supply generic type information for classes that don’t provide it themselves. For example, we may have a (third-party, or re-used) container that was not written with type information embedded in it. This would fail to work with our algorithm, because that requires type information. By providing an explicit specialisation of the ContainerTraits class for our third-party container we can ‘hard-code’ specific type information about our container, thus allowing it to work with our generic algorithm.
Dealing with raw arrays
It might be useful if our generic algorithm could work with so-called ‘degenerate’ containers – also known as: arrays. Arrays are effectively just pointers so we can use partial specialisation to provide type information for any pointer types.
Notice that the ContainerTraits this time don’t refer to a container type, but an object type.
However, there is a wrinkle: if the container is an array of constants then our value_type would also be const. Our algorithm is creating temporary objects by retrieving them from the container. Using value_type as a declaration would mean that the temporaries are const, too. Constant temporaries aren’t particularly useful so we have to do two things:
Provide a new typedef, temp_type, that clients can use for creating temporary objects
Provide a second partial specialisation, for const T*, that provides a non-const temp_type.
auto is your friend
Traits solve the problem of trying to determine type information from a supplied object. In C++11 we have the auto keyword.
Auto tells the compiler to deduce (and declare) the type of an object based on the expression used to initialise it. However, the semantics of auto are such that it deduces the ‘raw’ underlying type of the expression – that is, stripped of any references or type qualifiers.
You can explicitly qualify auto-deduced types by adding qualifiers to the declaration as required.
Qualifying an auto type with an ampersand (&) creates a reference-to-type object. The same rules as above apply but with the caveat that you cannot remove const-ness, for example, having a non-const reference to a const object – the compiler will force the deduced type to a const reference.
Below are some examples:
In our algorithm we can replace the traits information with auto-deduced types. The compiler will determine the type of our object based on the calls made to the container.
Using auto doesn’t replace the need for traits classes; but it does make a lot of algorithm programming a lot less complex (and more elegant).
Traits are a powerful way of abstracting type information, giving a clean separation between containers of objects and the algorithms that operate on them. In essence they are an extension of the old programming adage “You can always increase flexibility and reduce coupling by adding one more level of indirection”. The difference is that traits do this indirection at compile-time, rather than run-time, so they add no overhead to performance of the system. The cost, of course, is in increased compile times and complexity of code (and hence a potential reduction in maintainability!)
For many examples, however, code readability can be improved by using the auto keyword and allowing the compiler to deduce the type(s) during compilation from the template instantiation.
We’ve come to the end of our exploration of templates in C++. This isn’t the end of the story – there are aspects of template code (for example, meta programming) that we’re not going to cover.
That said, I hope this series of articles has given you the tools – and more importantly, the confidence – to explore templates in more detail, and to apply them to your project code.
Used carefully, and in the right place, templates can be a major asset to your architecture; and a very powerful tool in your programming toolkit.
If you’d like to know more about C++ programming – particularly for embedded and real-time applications – visit the Feabhas website. You may find the following of interest:
Latest posts by Glennan Carnie (see all)
- Technical debt - August 22, 2018
- Your handy cut-out-and-keep guide to std::forward and std::move - April 26, 2018
- Setting up Sublime Text to build your project - April 12, 2018