Getting Started with Templates
by [email="glenn@glennwatson.net"]Glenn Watson[/email]
In this article I am going to take you through a brief introduction of templates along with some of the common issues that programmers face.
What are templates?
Templates are an efficient way of allowing one piece of code to operate on many different types. They are written using unspecified types which are specified at the point of use. No performance overhead is added because templates are instantiated as classes at compile time not run time. Templates are a powerful mechanism that allows a programmer to implement a wide variety of classes and functions from a smaller code base.
A common example used to illustrate the use of templates is a mathematical vector class. A vector class should provide storage for the components of the vector (eg. xyz coordinates) as well as functions to perform basic vector calculations.
In an application we may have a need for a Vector class for ints in one place and for a Vector class for floats in another place. An object orientated programmer's first instinct might be to derive subclasses named IntVector3 and FloatVector3 from a base class named Vector3. However, inheritance is unnecessary, it demands extra code to replicate the same functionality for both int and float and extra unneeded code presents an opportunity to introduce extra unneeded bugs. Templates provide a much more elegant solution.
Here is an example:
template class Vector3 { private: T X, Y, Z; public: T GetX() const { return X; } T GetY() const { return Y; } T GetZ() const { return Z; } void SetX(const T& value) { X = value; } void SetY(const T& value) { Y = value; } void SetZ(const T& value) { Z = value; } }; Vector3 FloatVec; Vector3 IntVec; Above the class declaration, you'll find the template declaration 'template '. The keyword 'template' begins a template declaration with the template parameters specified inside the triangular brackets. Additional parameters are delimited by commas. In this example, you'll note that there is only one template parameter 'T'.
A template parameter has two parts, the type parameter and the identifier. The type parameter defines the identifier to be a typename if declared using the 'class' or 'typename' keywords or a template name if declared using the 'template' keyword. Using the 'typename' or 'class' keywords allows you to pass in any type whether it is a class, struct, enumeration, bool, integral, or floating-point type. The identifier portion of the template parameter gives the parameter its name. In the above example the identifier happens to be 'T', but you can use any valid C++ identifier to name a template parameter.
When a Vector template is instanced, each occurrence of the identifier 'T' in the class definition and implementation will be replaced by the type specified in the template declaration. In the case "Vector3 FloatVec;", 'T' is replaced with 'float' and in the case "Vector3 IntVec;", 'T' is replaced with 'int'. Where "T X, Y, Z" is defined in the class, the statement becomes "float X, Y, Z" and "int X, Y, Z" respectively and so to with the return types and parameters of the accessor functions. A lot more functionality could be added to this Vector class; however, I have kept it simple for the sake of illustration.
It is important to remember that Vector3 is not a class but a template. During compilation the compiler uses a template to create a particular classes based on the pattern that the template defines. In this example, Vector3 is not the class, Vector3 is the class as is Vector3. The compiler actually generates a new class for both.
You can have more than one type in a template. Define the extra types in the template definition statement.
For example:
template struct MyExampleStruct { T x; G Y; }; MyExampleStruct MyObject; In this example, 'T' will be replaced with an int and 'G' with a float. This example uses a struct instead of a class but the template syntax for both is the same. In C++ structs and classes are inherently similar items with the primary distinction that the default permission level of a class is 'private' while it's 'public' for a struct.
Another improvement that could be made to this Vector template is to not limit it to three coordinates. For example, at some point in the future you might want to use a Vector with four coordinates. In situations like this passing values can be useful.
For example:
template class Vector { T values[size]; }; Vector vector3; The size parameter in the template instancing declaration is used within the resulting class to create an array of the specified type and the specified number of elements - in this case, a float array of three elements.
Functions can also be made into templates.
For example:
template T foo(T& param) { return param + 1; }; void sample() { int delta; std::cout << foo(delta); }; In this example, the function "foo" uses 'T' as both a parameter type and a return type. In order for the function "sample" to make a call to "foo" a type argument must be supplied to the templated function. In this case, int is substituted for 'T'.
Template specialization allows you specify custom functionality for a particular type.
For example:
template T Max(const T value1, const T value2) { return value1 < value2 ? value2 : value1; }; template <> const char * Max(const char *value1, const char *value2) { return (strcmp(value1, value2) < 0) ? value2 : value1; }; The function above returns the maximum value between two values. The generic template t the top will do fine for most situations. We just do a simple comparison using the less than operator and return whichever value is greater. How about C style strings, where the greater than and less than operators have no relevant meaning? With template specialisation we can provide a generic implementation above that will be used for data types other than "const char*" and a specialisation that will be invoked whenever the string is passed in.
Templates are quite often used with container classes, containers being a class that hold a series of objects, similar to an array but with a lot more features and safety. Templates are quite suited to this task since they actually promote type safety. In C# and Java, the containers rely on the fact that all objects within those languages are derived from a single base class (normally called Object). These types of containers are called Polymorphic containers. The problem is if you want any sort of type safety you generally have to derive a class from the container class and instruct it to only allow in a certain object type. Since with templates it actually generates a new class/function based on the input type, it has all the type checking mechanism of any other C++ class/function. C++ comes with its own set of containers in the STL (Standard Template Libraries) libraries; examples include std::vector, std::deque, std::map and std::list. It is recommended that you use these containers over writing your own since the C++ designers have gone to great lengths to insure performance and it reduces the bugs you introduce by developing your own containers. That being said C# and Java are both introducing a similar mechanism to templates called Generics to address this issue.
Template Gotchas
You can't separate your class definitions into source and header files like you normally would
The problem is that most compilers do not remember the details between different source (.cpp, .cxx etc) files. As stated above, classes and functions that you define with templates aren't really classes and functions. They are considered template patterns and these patterns are used to generate the actual classes. This causes issues with the compiler, since templates aren't really classes, and only become classes when you actually declare them. The compiler can't actually allow you to have your implementation in your source file and header (h/.hpp etc) without a bit of tinkering around.
A lot of people like to put their actual implementation in the source file while keeping the class declaration in header files. This allows them to glance through the header file to see the functionality the class provides. However, this is not easy to achieve with templates.
One solution is to have all your code inlined within the header file. The main disadvantage to this technique is that you loose the benefit of separating the implementation from the declaration and the ability to see the functionality of a class at a glance.
What many people do is place both their implementation and declaration in the header file. They do not inline the actual implementation but rather keep it separate from the declaration.
For example:
template class MyClass { private: T x; public: T GetX(); }; template T MyClass::GetX() { return x; } One common mistake many people make when separating the code from the implementation, is that they forget that every bit of code outside the class declaration has to be within its own template declaration. Another common mistake is to declare the implementation just as you would inside the class like MyClass::GetX(). This is not correct. As pointed out before, once you use templates it's no longer MyClass, it's MyClass.
Another approach is to separate the implementation into another file. You include this file at the bottom of the header using the #include directive.
Some people keep the extension ".cpp". However this file must not be included in the compilation process and therefore a extension like ".tpl" is better. In Visual C++, for example, you have to make sure you do not include it in the project manager. Other compiler implementations make sure it's not within the makefile.
Another solution is to use the 'export' keyword to achieve this functionality; however, at this time only the Comeau compiler actually supports this keyword. Although there is a lot of talk in the standards community about removing the export keyword, it will remain in the next revision of the C++ Standard because it provides at least a conceptual benefit.
Templates with Smart Pointers
Smart pointers are objects that manage the destruction and aspects of a pointer's lifetime, and take care of a lot of the safety issues for you. Through the use of operator overloading they are designed to give you the "feel" of an ordinary pointer.
One smart pointer that the C++ standard provides is std::auto_ptr. The standard auto_ptr provides a lot of great functionality, however, one of its big problems is that you can't really use it with C++ containers or anything using a template. The C++ standard has defined using auto_ptr with templates and containers as undesirable due to the ownership transfer semantics.
STL Containers and smart pointers have become quite popular now-a-days. Containers like std::vector allow you the flexibility of allowing quick and easy storage for your objects and values without having to roll out your own solutions like linked lists. Smart pointers allow you to have your memory cleaned up efficiently without having to worry about the details yourself.
Containers like std::vector don't provide any sort of memory cleanup. If you use a std::vector with a pointer it's your responsibility to go through and cleanup the pointers it contains.
Consider using boost::shared_ptr from the boost suite of libraries. Not only will it work with templates and STL containers but it also has a lot of advanced features like reference counting etc. I have been using it myself for all my smart pointer needs. You can find information about it here: http://www.boost.org. Another popular Smart Pointer is part of the Loki library by Andrei Alexandrescu and you can find information about it here: http://sourceforge.net/projects/loki-lib/.
Some people get tripped up when defining a smart pointer and container combo. For example a lot of people might initially define it as this:
std::vector
Takes you through a brief introduction of templates including some of the gotchas.
Advertisement
Recommended Tutorials
Other Tutorials by Myopic Rhino
26568 views
23772 views
Advertisement