6. Template as double-edged sword: basic knowledge
In previous articles I often used std::auto_ptr<T> or other smart pointers. I explained only how to work with them but I didn't tell you how they provide handling of pointers of different types. Also every time I wrote <T> sign.
Yep, in this article I'm going to describe technique of templates in C++. C++ standard library is full of templates and I have already mentioned lots of stuff therefrom, but without information on how it works.
After struggling with pointers and references, it's time to explain next powerful and unclear thing in C++ - templates.
or light introduction to templates
Let's begin from the simplest one. Imagine that we have the following function for calculating sum of two squares:
Hm, okay. Few days later you understand that you need the same function, but for double type and you write this:
Hm, so now you have two functions in your project which differ only in type. Also you need to maintain these two functions: if you find a bug in one function, you should remember about fixing the other one. So this is the first place where templates are your friends:
You see syntax of templates above. It consists of:
template - keyword which means beginning of a template declaration;
<typename DataType> - list of comma-separated template arguments. In our case it has one argument with name DataType, typename is another keyword which can be replaced with “class” keyword, but typename is more preferable in most cases as it's more general.
Next lines are usual declaration of a function with one difference: as type we have used DataType everywhere.
Lets see how to use it:
// intResult will be 20
// Full call of template is SquareSum<int>(intX, intY)
// doubleResult will be 10.25
// Full call of template is SquareSum<double>(doubleX, doubleY)
As you can see we used template function as usual, but full calls of SquareSum() as I said in comments are:
<int> and <double> specify type of template argument DataType. When compiler faces the call of template function it generates function with necessary types. If it is already met, compiler gets predefined function. So instead of writing function with different types manually compiler as humble servant does it for us.
As for why it was enough to write SquareSum(intX, intY) or SquareSum(doubleX, doubleY), modern compilers have template argument deduction. My compiler gcc is smart, it deducts types. If it can't do it, it will print a compiler error for you and probably you will need to specify template arguments directly in <> after template function.
Currently we have function with the same type for two arguments x and y. Let's extend our template argument list:
As you probably guessed we have an issue with return type of the function. See such calls of our new template functions:
// badIntResoult will be 10
// Full call of template is SquareSum<int, double>(intX, doubleY)
// betterDoubleResult will be 10.25
// Full call of template is SquareSum<double, int>(doubleY, intX)
Here we get different results by the same data, because generated template for SquareSum(intX, doubleY) call return into type which cuts off result to integer value. In second case everything is okay because return type is double. So you must beware of such situations, because it is restriction of templates for functions which you can't avoid.
or increase templates issues
Classes can be used as template too, just as functions. Let's see implementation of template class which encapsulates dynamic array and provides custom methods for its handling:
Actually this class is wrapper for std::unique_ptr<T> just for showing common work with templates in class declaration. As you see declaration of template class begins with template keyword and template argument list as template function does. Next thing which you must observe is that template argument DataType is used as:
type for memory allocation (1.1);
calling default constructor (1.2);
argument type of method (1.3);
return type of method (1.4);
type for other template class (1.5);
Possibility (1.2) is very useful when you need to initialize template variable with its default value which I use for initializing every item of array by zero.
Let's see usage of such class now:
// It prints: 0 0 0 0 0 0 0 0 0 0
// It prints: 1 2 3 4 5 6 7 8 9 10
In line (2.1) we have declared variable with template class type. Compiler can't deduct template arguments here so we set them directly by <int>. After this line we work with arrayKeeper as usual instance without any changes.
One of disadvantages of template classes (functions have the same) is that most probably you will declare them only in header files. Because of template nature you can't put implementation to a separate source file. It compiles but causes errors during linking such code.
There is a way with template exporting (use export keyword with template), but mainstream compilers don't support it. Actually it is implemented in a few not really popular compilers and unfortunately C++ community doesn't accept this because of some issues with this technique.
Also there is another way to write implementation of templates in source files through explicit instantiation. Look at my example:
In header file you see that we have just defined template class and its constructor. In source file at (3.3) we have declared template constructor of template class and at (3.1), (3.2) we have explicitly set possible types of template. Without these code (3.4) and (3.5) won't be linked, it will cause linker errors, but with the help of explicit instantiation we successfully avoid it.
I think you understand that on every usage of template class with new types it will force us to write explicit declaration of every method of class in source file. So most programmers don't like this overhead (which also limits use cases of the file) and just leave implementation of template classes in header files. Likely you will also act in the same way.
Bad side of templates
or on another side of the Moon
At the moment templates look great, but unfortunately if we get deeper, you will understand:
1. Usage of templates makes build process slower, because of lots auto-generated code and type deduction.
2. It is hard to debug auto-generated function\method. It isn't an easy task for debugger to understand correctly auto-generated code at run-time.
3. Code bloat - every auto-generated code increases size of object\executable file.
4. You can't easily separate code entity into definition in header and declaration in source file. Thus it's not good to use them for libraries when you want to hide implementation.
5. Also every minor change in header file with template class usual causes rebuild of big part of a project.
6. Nested templates (template in template I will talk about this in next articles) are not supported by all compilers or they have restriction for level of nesting.
7. Default compile error messages with complicated templates are usually huge, which makes it harder to understand what's going on.
As you can notice besides powerful force of templates which might help you, it has long list of disadvantages. They make you wait longer during building process and in some cases it makes your life harder.
In this topic I began talking about primary technique which is used in C++ standard library. I have already used it through smart pointers and std::vector<T> in my previous articles. I have shown that templates are very powerful for programmer for writing less code, but on the other hand it can force you to take a challenge. Indeed templates are double-edged sword.
In my next articles I will continue talking about templates and will get deeper and deeper in their force and madness. I am going to talk about usage of templates with OOP stuff, keyword typename, specification of templates, duck typing and calculations at compile time.
Examples of code from article you can find in my Github repository.
My previous articles from “.CPP files” series are: