Let's imagine we are creating a library of useful functions to help our team. We start with this:
int Average(int x, int y) {
return (x + y) / 2;
}
This function behaves like we expect when passed an int
:
Average(2, 4); // 3
But an obvious issue crops up when passed a float
:
Average(1.9f, 1.5f); // 1
Our floats both get converted to the integer 1
, so our function incorrectly reports the average of 1.9, and 1.5 is 1
.
We could fix this by creating another version of this function that accepts float
values. But what if someone passes a double
, or long
?
Our colleagues could also create an entirely new numeric data type. We'd have no way of knowing what it is in advance, so our library would become useless.
The solution to this is a template type. Applying this to our function looks like this:
template <typename T>
T Average(T x, T y) {
return (x + y) / 2;
}
In this example, we are telling the compiler that T
is a template type. Then, any time we use T
within our function, that is to be replaced by the appropriate type.
We specify the type when we call our function:
Average<int>(3, 5); // 4
Average<int>(1.9f, 1.5f); // 1
Average<float>(1.9f, 1.5f); // 1.7f
Average<int>(3, 4); // 3
Average<float>(3, 4); // 3.5f
It's important to understand what is going on here, as there's often some confusion at this point. Templates are not an implementation of dynamic types. Everything still uses static types - templates are just a convenient compiler trick. Templates let the compiler create many different versions of our function at compile time.
When the compiler sees us calling Average
with integers, it instantiates a version of our function that uses integers. When it sees us calling it with floats, it creates a new version of our function that uses floats.
This continues until the compiler has created enough variants of our function to support all the different ways in which it is called throughout our application. This is an example of compile-time polymorphism.
In other languages, this concept is often called generics, and the practice is known as generic programming.
When we're using function templates, it's not necessary that every parameter be a template. We can mix and match the template and non-template parameters as required:
template <typename T>
bool MyFunction(T Param1, int Param2) {};
It's not always necessary to specify which version of a templated function we want to use. For example, instead of writing this:
Average<int>(3, 5);
We can simplify it into this:
Average(3, 5);
The compiler can see we are calling our function with 2 integers, so it can infer what we want. This is called template argument deduction. There are some scenarios where this won't work.
For example, our Average
function has only one template parameter - the type we called T
. This means that both parameters and the return value all need to have the same type. If we call our function with two different types, the compiler can't deduce what it needs to do:
Average(3, 5.0);
We could explicitly state the template type:
Average<float>(3, 5.0);
Or static_cast
to help the compiler deduce what we want:
Average(static_cast<float>(3), 5.0);
But a better solution, in this case, would be to update our template function to be more flexible. We can do that using multiple template types.
Let's update our Average
function to be a bit more flexible:
template <typename TFirst, typename TSecond>
TFirst Average(TFirst x, TSecond y) {
return (x + y) / 2;
}
We can now calculate the average between two different numeric types, and use template argument deduction to keep things simple:
Average(2.5f, 2); // 2.25
If we wanted to specify the template types, we could do that using the similar <
and >
syntax, using a comma:
Average<int, int>(2.5f, 2); // 2
We can also specify just one, letting the compiler deduce the second:
Average<float>(2.5f, 2); // 2.25
Unfortunately, there is still a problem here:
Average(2, 2.5f); // 2
Our function's return statement is correctly calculating 2.25
. However, our template is set up such that the type of the first argument (TFirst
) is also used as the return type. So, this call is invoking a function that has a return type of int
, causing our 2.25f
to be returned as 2
.
We could fix this by converting the first argument to a float:
Average(static_cast<float>(2), 2.5f); // 2.25
We could also fix it by not using template argument deduction. Rather, we could state we want to use the <float, float>
version of the function:
Average<float, float>(2, 2.5f); // 2.25
// This would also work
Average<float>(2, 2.5f); // 2.25
But generally, the best fix is to update our template function to not lock down the return type at all. Instead, we can use auto
and let the compiler figure it out:
template <typename TFirst, typename TSecond>
auto Average(TFirst x, TSecond y) {
return (x + y) / 2;
}
Average(2.5f, 2); // 2.25
Average(2, 2.5f); // 2.25
There is another way of creating function templates. We can simply specify our parameter types as auto
:
auto Average(auto x, auto y) {
return (x + y) / 2;
}
This is called an abbreviated function template. It is a relatively new addition to the language, added in C++20
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.