In previous lessons, we’ve seen how class templates are recipes that the compiler can use to create classes. In this lesson, we’ll see how these same principles apply to function templates. We will learn:
Let's imagine we are creating a library of useful functions to help our team. We start with a basic utility that helps us calculate the average of two numbers.
This function behaves like we expect when working with int
objects:
#include <iostream>
int Average(int x, int y) {
return (x + y) / 2;
}
int main() {
std::cout << "Average: " << Average(2, 4);
}
Average: 3
But if someone tries to use it with floats, they’ll get the wrong answer:
#include <iostream>
int Average(int x, int y) {
return (x + y) / 2;
}
int main() {
std::cout << "Average: "
<< Average(1.9f, 1.5f);
}
Average: 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
, or a completely new type they’ve created?
As we’re likely predicting from previous lessons, the solution here involves using a function template. As with our class and variable examples, we create a function template by specifying a template parameter list, and then use those parameters within our template.
Below, we create a template that takes a single typename
parameter, which we’ve called T
. That typename
is used in three places in our template function - it is used as the return type, and the type of both of our parameters:
template <typename T>
T Average(T x, T y) {
return (x + y) / 2;
}
What we mean by "parameters" can get quite confusing when working with template functions, as we have two sets of parameters.
We can identify a list of template parameters by the <
and >
that surrounds them, whilst function parameters use (
and )
.
In the following example, T
is a template parameter with a type of typename
, whilst x
and y
are function parameters with a type of T
:
template <typename T>
T Average(T x, T y) {
return (x + y) / 2;
}
Similarly, when using our template, we have both template arguments and function arguments.
Remember, a parameter is a variable within our template or function. An argument is the value we pass into that variable when we invoke our template or function.
We can similarly distinguish template arguments from function arguments based on whether they’re within <>
or ()
In the following code, we use int
as a template argument, whilst 1
and 2
are function arguments:
Average<int>(1, 2);
Now that we’ve defined a function template, we can instantiate this template as needed to create a function at compile time.
We provide the template argument, and the compiler instantiates our template to create a function. It substitutes the argument we provided into every location in our template where we used the parameter.
Let’s imagine we invoke the template, using an expression like Average<int>
:
template <typename T>
T Average(T x, T y) {
return (x + y) / 2;
}
int main() {
Average<int>;
}
Behind the scenes, we can imagine the compiler generating a function. Within this instance of the Average
template, everywhere T
appeared will be replaced with int
, yielding a function that looks like this:
int Average(int x, int y) {
return (x + y) / 2;
}
We can immediately invoke the function returned by our template using the ()
syntax, and providing arguments if needed. Here are some examples:
template <typename T>
T Average(T x, T y) {
return (x + y) / 2;
}
int main() {
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 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 to this:
Average(3, 5);
The compiler can see that the function created by our template is eventually going to be called with 2 integers, so it can infer what template arguments are needed to support that. As such, it will instantiate an Average<int>
in this example.
If we call our function with two different types, the compiler can't deduce what it needs to do:
Average(3, 5.0f);
We could explicitly state which argument we want to instantiate the template with:
Average<float>(3, 5.0f);
Or we could conform our argument types such that template argument deduction can work. Below, both arguments have the float
type, so the compiler will use the Average<float>
function:
Average(float(3), 5.0f);
As with other template types, function templates can accept multiple parameters, separated by commas. Below, we’ve updated our template such that x
and y
can have different types.
We’ve set the return type to T1
(matching x
) for now, but we’ll see better approaches later in this lesson:
template <typename T1, typename T2>
T1 Average(T1 x, T2 y) {
return (x + y) / 2;
}
When instantiating our template, we also separate multiple arguments using commas. Below, we instantiate our template by passing int
and float
as explicit arguments:
Average<int, float>(3, 5.0f);
Or equivalently, using template argument deduction:
Average(3, 5.0f);
Template parameters can have default values, making them optional arguments. Below, we give the user the option of providing T2
but if they don’t, it will be int
:
template <typename T1, typename T2 = int>
T1 Average(T1 x, T2 y) {
return (x + y) / 2;
}
int main() {
Average<int>; // Average<int, int>
Average<float>; // Average<float, int>
}
The default values can also be values that appeared earlier in the argument list. Below, we give the user the option of providing T2
but if they don’t, it will use the same value as T1
template <typename T1, typename T2 = T1>
T1 Average(T1 x, T2 y) {
return (x + y) / 2;
}
int main() {
Average<int, float>; // Average<int, float>
Average<int>; // Average<int, int>
Average<float>; // Average<float, float>
}
If we want to instantiate a template using all the default parameters, we can either provide an empty argument list using <>
, or omit it entirely:
template <typename T1 = int, typename T2 = T1>
T1 Average(T1 x, T2 y) {
return (x + y) / 2;
}
int main() {
Average<>(1, 2); // Average<int, int>
Average(1, 2); // Average<int, int>
}
In the previous examples, we set the return type of our function template to be the same as the first template argument, T1
.
But, this is not entirely desirable. The return type of (x + y) / 2
is not necessarily going to be the same as the type of x
.
Function templates are one of the main scenarios where we will use the auto
return type, letting the compiler automatically determine what type each of our template instantiations will return:
template <typename T1, typename T2>
auto Average(T1 x, T2 y) {
return (x + y) / 2;
}
Earlier in this chapter, we introduced the decltype
specifier. This would also allow us to determine what type (x + y) / 2
results in, without us needing to know the types of x
or y
in advance:
template <typename T1, typename T2>
auto Average(T1 x, T2 y) {
using ReturnType = decltype((x + y) / 2);
return (x + y) / 2;
}
We can use a decltype
specifier as the return type of a function, but using the traditional function syntax, it won’t have access to the function parameters. This is because the parameter list comes after the return type:
template <typename T1, typename T2>
decltype((x + y) / 2) Average(T1 x, T2 y) {
return (x + y) / 2;
}
error C3861: 'x': identifier not found
error C3861: 'y': identifier not found
C++ provides an alternative way to specify the return type of functions, to solve exactly this use case. We place auto
in the traditional return type position.
Then, we insert a trailing return type after the parameter list, separated by an arrow ->
. It looks like this:
template <typename T1, typename T2>
auto Average(T1 x, T2 y)
-> decltype((x + y) / 2) {
return (x + y) / 2;
}
We cover trailing return types in a dedicated lesson later in the course:
When our function is only using our template parameters within its function parameter list, there is a simpler syntax we can use.
It involves using the auto
keyword as our function parameter types. For example, instead of this:
template <typename T1, typename T2>
auto Average(T1 x, T2 y) {
return (x + y) / 2;
}
We could write this:
auto Average(auto x, auto y) {
return (x + y) / 2;
}
This is called an abbreviated function template and it is a relatively new addition to the language, added in C++20
In this lesson, we covered function templates. The key takeaways are:
<>
and used within the template bodyauto
keyword can ask the compiler to determine the return type, and is particularly useful when working with function templatesUnderstand the fundamentals of C++ function templates and harness generics for more modular, adaptable code.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.