C++ Lesson 5: What are C++ templates for
This lesson is about why templates exist in C++, and why they are very useful. This lesson is not a detailed explanation of all their technical facets. For that there are excellent books available.
Without templates: containers
So why are there templates? Consider the following. Imagine you need to develop a sales application that handles all kinds of different data: customers, orders, and so on. This data will be logically divided into all kinds of classes, many of which are not related in any way (i.e. they do not all extend from one base class). Since there will be more than one instance of each class, very soon you'll need containers. For example, arrays of orders, unique collections of addresses (a set), maps with different keys to data, for example order numbers to order objects, and so on.
struct Address
{
string Address;
string Zipcode;
string City;
string Country;
};
class Customer
{
string mName;
Address* mAddress;
public:
Customer( const string& name, Address* address );
Customer();
};
class Order
{
int mNumber;
Customer* mCustomer;
public:
Order( int order_number, Customer* customer );
};
class
Read the code above, and let's step back for a moment. Consider the following: since you are a lazy coder, you do not want to write multiple containers that basically do the same thing (e.g. multiple vector implementations to hold different types of data). So in the vector container you'll need to write, what is going to be the data type? In other words, what are those containers going to contain? Objects of class
Address
? That's fine for addresses, but how will you store arrays of customers and orders? Pointers to Address objects, e.g.
Address*
? This gets you a little closer, since you could simple lie to the compiler and cast pointers to other objects to pointers to Address objects, but it would be a massive risk and you'd be losing your type safety.
So, you're clever, and you think: why not take pointers to "nothing in particular", or pointers to "anything", i.e. the
void*
pointer? You would still have lost your type safety, but at least you wouldn't have to lie so hard to the compiler anymore. In fact, it is this approach that is used by languages that do not support templates, such as Delphi (at least until Delphi 5, I have no clue if later Delphi's support templates). In C++ you end up writing a container like this:
class Vector
{
void** mArray;
size_t mCount;
public:
void add( void* data);
void* get( size_t index );
void* remove( size_t index);
}
And then use it like this:
Vector orderVector;
Customer* customer = /* ... */;
orderVector.add( new Order( 1004, customer ) );
orderVector.add( new Order( 1005, customer ) );
...
Order* o1004 = reinterpret_cast<Order*>( orderVector.get( 0 ) );
Order* o1005 = reinterpret_cast<Order*>( orderVector.get( 1 ) );
The
reinterpret_cast<>
is the C++ way of doing casts: they stand out being a weird keyword, they are highlighted in modern IDE's, and as such grab your attention way quicker than the C way of doing casts. But that was not my point. Think of this: what is to stop you from accidentally adding a pointer to a
Customer
object to your order vector? NOTHING! The compiler will not complain, and you wouldn't even need to lie to him. Why? Because a pointer to a
Customer
is just as valid a value for
void*
as is a pointer to an
Order
object! Let this sink in for a moment. Since you wanted to have a single implementation of each container type, you HAD to go for the most basic type C++ knows: anything, a.k.a.
void*
. And as soon as you convert a pointer to something (e.g. an
Order*
) to a pointer to anything (
void*
), the compiler cannot help you out anymore. You lose type safety, you lose coding assistance in your IDE (i.e. it cannot help you with autocompleting methods and members anymore while you're coding), you lose anything that's worth anything!
With templates: the basics
So, you ask, is this why there are templates? Yes. This is one of the many reasons why templates can be of such great help. With templates, you solve all of the above issues with grace and power. But how?
You can have template classes (and thus also structs), and template function (and methods). They are basically pre-defined molds to be completed with types. Let that sink in. They are molds to be completed with types. This implies that types, or at least the types required to "complete" the template, are very important. They are, and they are even so important, it's the first thing you write after the template keyword:
template<typename _T, typename _U>
This is not the complete syntax for a template class or function, but I want to focus on the part between the < and >. It's the list of types (and values, but more about that later) needed to complete the template. It's important that you store this in your memory, because template syntax can be very daunting to programmers who are new to the concept. But when you know where to look for the type list, you're off to a great start.
So what does the above mean? For the coder of the template, it means that there will be two types, one named _T (but while writing the code you have no clue what will be in it), and one named _U. For the user of the template, it means that you will have to supply two types. What the types actually do inside the template, you'll have to extract either from the code, or from the documentation.
Without a more extensive example, this doesn't make much sense. So let's continue and write a template function.
template<typename _T, typename _U>
_U multiply( _T valueA, _T valueB ) {
return static_cast<_U>( valueA * valueB );
}
So what did we just write? We wrote a multiplication function that can multiply two values, FORCE the user to specify the arguments in the type he wants, but both will have to be the same, and get a return value in the type of his own choosing as well. Let's see how this might be useful:
int i = 3;
int j = 5;
char c = multiply<int,char>( i, j );
This multiplication is performed with type safety: the arguments to function
multiply<int,char>
are
int
's, as the first template parameter, _T, which is used as the type for the two function parameters, is set to int, and the result is a char (_U in the template). The function itself handles the conversion by doing a proper C++ cast.
Another example, useful in gaming engines:
template<typename _T>
_T clamp( _T value, _T min, _T max ) {
if (value < min)
return min;
if (value > max)
return max;
return value;
}
This function is capable of clamping a value to a user given range. It's not only able to do that, but it's also able to do so in a type safe way for ANY type that supports the < and > operators! It can clamp ints, floats, doubles, in theory even strings (althought that doesn't make much sense semantically). And all that, within a few lines of code (that technically could have been written a lot shorter, I kept it this long as to not confuse you too much!) This is the power of templates!
With templates: containers
But now that I've covered some of the basics, let's return to our order system. You probably already have some clue as to how this concept is going to help your
void*
-container problem. Yes, all you need to do, is make a template out of the Vector implementation, and replace void with a template parameter type!
template<typename _T>
class Vector
{
_T* mArray;
size_t mCount;
public:
void add( _T* data);
_T* get( size_t index );
_T* remove( size_t index);
}
We use this class as follows:
Vector<Order> orderVector;
Customer* customer = call_function_to_get_customer();
orderVector.add( new Order( 1004, customer ) );
orderVector.add( new Order( 1005, customer ) );
...
Order* o1004 = orderVector.get( 0 );
Order* o1005 = orderVector.get( 1 );
Especially notice how the get() calls no longer require a cast, as their result types for
Vector<Order>
is declared to be an
Order*
! Additionally, you will no longer be able to accidentally store
Customer*
's in your order vector, as now your compiler knows you meant to store
Order*
only, and will tell you when you try to store anything unrelated to
Order
objects, such as
Customer
objects!
Note that I have chosen to use pointers to the user specified type. In other words, I'm using
_T*
's, not
_T
's. This is a choice for this particular template, but you are not forced (or limited) to this. You can use the
_T
type in any way you would use a normal type: const, non const, reference, pointer, anything.. If it's a class, you could even call a static function:
_T::someFunction();
. Ofcourse, when the type specified by the user doesn't contain a static function
someFunction
, or when it even isn't a class or struct at all, this wouldn't compile!
But there's more
With what I've told you here, you've seen only a fraction of what templates can do. There's (partial) specialization of templates, there's templates with template parameters (yes :-P), there's functional - compile time - programming with templates, there's templated methods within templated classes, and so on. There's a whole world of wonderful things. But if you're new to template programming, I think for now you'll be satisfied for quite some time. I urge you to try stuff out: write your own vector, even though a better one already exists (STL's std::vector). Write your own mathematical functions (although better ones exist). Write your own everything when it comes to templates, simply because it's the only way to really know what their power is (and what isn't!).
One last treat before I stop: beside type parameters, templates can also have value parameters. For example, to write a user-defined mathematical vector class, write the following:
template<typename _T, int _S>
class MathVector
{
_T mElements[_S];
public:
MathVector();
const _T& operator[]( size_t index ) const {
return mElements[index];
}
_T& operator[]( size_t index ) {
return mElements[index];
}
int size() {
return _S;
}
}
Want a 2D vector with integers, because you want to store pixel locations? Use
MathVector<int,2>
. Want a 3D vector with floats for entity positions in your game engine? Use
MathVector<float,3>
. Want to work with your vector?
MathVector<int,2> vector;
vector[0] = 1; // write it
vector[1] = 0;
printf("%d\n", vector[0]); // read it
Templates are awesome. Templates are what make C++ the most fun language on the planet. Yes. Yes. No you're wrong. Yes, it does. I love this language ^^
Some useful links:
Another template example: OpenGL vertex renderer
Without further information, if you're interested in reading up on how templates can also be used, view
this file.
0 comment(s)