C++ requires us to declare variables, functions, and most other kinds of entities using specific types. However, a lot of code looks the same for different types. Especially if you implement algorithms or if you implement the behavior of data structures. The code looks the same despite the type used. If a programming language doesn’t support a special language feature for this, you only have bad alternatives:
- You can implement the same behavior again and again for each type that needs this behavior.
- You can write general code for a common base type such as Object or void*.
Templates are a solution to this problem. They are functions or classes that are written for one or more types not yet specified. When you use a template, you pass the types as arguments, explicitly or implicitly and the compiler instantiate the typed version of the function or class at compile time. As this is done at compile time it increases the time it takes to compile such codes.
However during my time as application developer I have rarely encountered codes with templates. This is because, this technique is mainly used for developing generic libraries. But that doesn’t mean application programmers can’t take advantage of this powerful technique. If your sole focus is performance and you are not ready to even give the extra time it takes to access member functions through virtual tables then you can look for template based design patterns. To start with you can look at Policy Based Class Design, which can come really handy.
Here I just want to focus on Function Templates. We will go over examples and see its features and various implementations techniques from C++98, 11 & 14. Here is what we will do to go over this topic.
- Write a max() function template that takes a single template parameter for its two function parameters
- Write a max() function template that takes two template parameters instead of one (one for each function parameter)
- Overload the max() function template for integers
- Specialize the max() function template(s) for integers
- Look at function call resolution technique used by compiler
Function with single template parameter
template <typename T>
inline T max(const T& lhs, const T& rhs) {
return (lhs > rhs) ? lhs : rhs;
}
<typename T> is called template parameter list. It can have type parameters or non-type parameters.
inline T max(const T& lhs, const T& rhs), is the function signature with template parameters. Template parameters of type T are deducted based on given arguments. They are deduced independently, if the types matches the function can be instantiated for that type.
In your code you can call the above function in following ways:
max(1, 2);
max('b', 'a');
max(5.3, 7.4);
max<>(6, 3)
max<>(6, 3), explicitly tells compiler to use the function template and deduce the type based on given argument.
But what happens if we max() as below:
max(1, 3.2);
This will lead to error, as the compiler will not be able to deduce the correct data type to use. However, if you tell the compiler the datatype to use it will not give error. See below:
max<int>(1, 3.2); // Explicitly telling the compiler to use int as datatype
max<double>(1, 3.2); // Explicitly telling the compiler to use double as datatype
Function with two template parameters
In template parameter list, we will use two parameters T and U. See below:
template <typename T, typename U> inline ??? max(const T& lhs, const U& rhs) { return (lhs > rhs) ? lhs : rhs; }
Now we have a problem, what should be the functions return type? The compiler cannot deduce the return type by itself so it needs our help. There are 5 different solutions to this problem depending upon the version of C++ you are using.
- A third template parameter (C++98)
- A return type deduction based on arguments (C++11)
- With common_type type trait (C++11)
- Using auto keyword (C++14)
- Using decltype(auto) (C++14)
With higher versions the solutions become more elegant.
Third template parameter (C++98)
This solution exist since C++98. Here we introduce a 3rd template parameter R to define the return type.
template <typename R, typename T, typename U> inline R max(const T& lhs, const U& rhs) { return (lhs > rhs) ? lhs : rhs; }
Now, the above function can be called as:
max<double, int, int>(1, 2.3)
max<double, int, double>(1, 2.3)
Here we are telling the compiler all the datatypes. We can also used a specialized call, where we are just letting the compiler know the return type, as it cannot be deduced. However the arguments types are implicitly deduced.
max<double>(1, 2.3)
max<int>(1, 2.3)
Of the two calls, the first one is more correct as there won’t be any information loss due to type conversion.
Note: what would happen if the return type is mentioned at the end of template parameter list? (in above example it is at the start of the list).
template <typename T, typename U, typename RET> inline RET max(const T& lhs, const U& rhs) { return (lhs > rhs) ? lhs : rhs; }
I won’t advise doing it. With this we loose the option to let the compiler deduce the argument types. As we must explicitly mention the return type, we can’t do it without telling the argument types. This is just because of the template parameter’s position.
Return type deduction based on arguments (C++11)
template <typename T, typename U> inline auto max(const T& lhs, const U& rhs) -> decltype(lhs + rhs) { return (lhs > rhs) ? lhs : rhs; }
With this implementation you can do all the below calls and the return type will be auto detected to double.
max(1, 2.3);
max(1.2, 2.3);
max(1.2, 3);
With common_type type trait (C++11)
This implementation is very similar to the one above with a slight difference. It uses type traits to identify the common type between the group of types provided, i.e. T & U in our case.
template <typename T, typename U> inline auto max(const T& lhs, const U& rhs) -> typename std::common_type<T, U>::type { return (lhs > rhs) ? lhs : rhs; }
With this you can make the same calls as before:
max(1, 2.3);
max(1.2, 2.3);
max(1.2, 3);
To view the return type you can use below code:
#include <typeinfo>
std::cout << typeid(max_3(2, 1)).name(); // prints i (for integer)
std::cout << typeid(max_3(2, 1.2)).name(); // prints d (for double)
Using auto keyword (C++14)
template <typename T, typename U> inline auto max(const T& lhs, const U& rhs) { return (lhs > rhs) ? lhs : rhs; }
Using decltype(auto) (C++14)
template <typename T, typename U> inline decltype(auto) max(const T& lhs, const U& rhs) { return (lhs > rhs) ? lhs : rhs; }
Overloading function template
Function templates can be overloaded. In presence of overloaded function template, the overloaded one will be called. Here we overloaded the function template for integers.
// Template version of max
template <typename T, typename U>
inline auto max(const T& lhs, const U& rhs)
-> decltype(lhs + rhs) {
return (lhs > rhs) ? Lhs : rhs;
}
// Overloading for int
inline int max(const int& lhs, const int& rhs) {
return (lhs > rhs) ? Lhs : rhs;
}
In this case:
Function template will get instantiated for below calls:
max(1.2, 2.3);
max('a', 'b');
max(1, 2.3);
max<>(1, 2);
Overloaded method will get called:
max(1, 2);
In the above calls when we mentioned max<>(1, 2), we explicitly asked the compiler to use the templatized version.
Specialized function template:
Say we have a function template but for a particular case or datatype we want the implementation to be different. It can be done with help of specialized function template. In below code the 2nd function is an example of specialized function template for integers.
// Template version
template <typename T>
T intMul(const T& a, const int& b) {
std::cout << "Multiplying: " << a << " with " << b << "\n";
return a * b;
}
// specialization for type int
template <>
int intMul(const int& a, const int& b) {
std::cout << "Specialized Multiplying: " << a << " with " << b << "\n";
return a * b;
}
It is different from overloading. The statement template <> before the 2nd method makes it a specialized form of the function template defined above. If we call intMul() as shown below:
std::cout << intMul(2, 3) << "\n";
std::cout << intMul(3.2, 4) << "\n";
their output will be as follows:
Specialized Multiplying: 2 with 3
6
Multiplying: 3.2 with 4
12.8
Function call resolution
Steps taken by the compiler to resolve function calls:
- Name resolution: Select all (visible) candidate functions with a certain name within the current scope. If none is found, proceed into the next surrounding scope.
- Overload resolution: Find the best match among the selected candidate functions:
- If a non-template is a perfect match, select it
- When a template is available, try to make it a perfect match
- If this is possible, select it
- Else, try to find the best match among the non-template functions; use argument conversions as allowed and necessary.
- If a template is selected, take all its specialization into consideration