Function Templates
Let's prevent all that code duplication!
By learning MORE C++ features??!!
Yes. We'll learn a way to use the same code for multiple types.
A function template is essentially a recipe for generating a function. You can use a variable for a type (or types) and then the compiler can fill that variable in with various real types to generate different versions of your function!
Our fill
function is a bit complicated, so let's start simple:
Declaration
template <typename T>
void printVal(T x);
Definition
template <typename T>
void printVal(T x) {
std::cout << x << std::endl;
}
This function template tells the compiler how to make a printVal()
function for any type T
the user might use.
In this example T
is a template parameter. You can think of it like a variable, but where regular variables hold values and exist when the program is running, the template parameter holds a type and is used when the program is being compiled.
The compiler knows which versions of printVal
to generate and compile based on the specific types used to call it in your code!
For instance, if we call
printVal<int>(42);
printVal<string>("Moo");
then the compiler will generate and compile both printVal(int x)
and printVal(string x)
.
Hay! What if I have a class
Unknowable
that doesn't support<<
? Can I still make aprintVal<Unknowable>
function and print it?
What If T
Isn't Compatible with the Code?
If we try to generate and compile a version of printVal
that doesn't compile, we will get a compiler error!
For instance, if we call
Cow mabel{5, 3};
printVal<Cow>(mabel);
then we will get a compiler error, because our Cow
class doesn't work with the <<
streaming operator!
Specifically, the error is:
template-fns.cpp:7:10: error: invalid operands to binary expression
('std::ostream'; (aka 'basic_ostream') and 'Cow')
cout << x >> endl;
~~~~ ^ ~
template-fns.cpp:43:5: note: in instantiation of function template
specialization 'printVal' requested here
printVal<Cow>(mabel);
^
Notice that the error lists two different places in the test file: one is in the template code itself (the line that didn't work) and one is telling you the line in the code that caused the compiler to stamp out the printVal
function template for the Cow
type.
Huh. Let me try… AAAAUUGH! The compiler just printed like seven pages of errors!!
Oh, yeah, that's true. The real error message is much longer than that snippet we showed you.
After the errors shown above, the compiler goes on to tell us in excessive detail about all the different types that operator<<
can work on that it tried and failed to convert a Cow
into.
There might be times when this amount of detail is important for tracking down a problem. Most of the time it's just a ton of useless information that you can safely ignore.
But this example actually tells us an important Golden Rule of Error Messages:
Always start with the first compiler error.
The last part of a compiler error message (or the last error) is usually way less informative, and is sometimes resolved just by fixing the first thing!
Sometimes there are so many errors that the first one has scrolled off the screen. What do I do then?
Most people see that problem in the terminal inside Visual Studio Code, because it defaults to retaining a very small number of lines of output. See this stack overflow post for how to fix that.
Type Deduction
For function templates, instead of saying
printVal<int>(42);
printVal<string>("Moo");
you can just call
printVal(42);
printVal("Moo");
and the compiler will deduce the type T
from the types of the parameters.
Multiple Template Parameters
This is cool! What about
fill()
? Could we do that?Sure!
You can also have multiple template parameters for a function template. For example,
template <typename Iter, typename Value>
void fill(Iter start, Iter pastEnd, Value value) {
for (Iter iter = start; iter != pastEnd; ++iter) {
*iter = value;
}
}
Here we have two template parameters: one for the type of iterator and one for the type of value to be assigned. So now we can call
std::vector<int> v;
fill(v.begin(), v.end(), 42);
std::list<string> s;
fill(s.begin(), s.end(), "Moo");
This code will even work with pointers, because the syntax is the same!
size_t size = 5;
int* arr = new int[size];
fill(arr, arr + size, 0);
Pass by Reference
Here's our printVal
function template again:
template <typename T>
void printVal(T x) {
cout << x << endl;
}
There is one small problem with it.
This function will make a copy of the argument that is passed in! Making a copy is fine if T
is a simple type like int
or float
, but what if T
is a class and x
is an object? Those could be very large, and it could be very expensive to copy them unnecessarily.
Generally speaking, because we don't know what type(s) we will be working with, it is better to take parameters as const
references instead of copying them, which allows us to avoid inadvertently copying large objects.
Thus our new and improved version would be
template <typename T>
void printVal(const T& x) {
cout << x << endl;
}
Practice
Here's a function specifically for int
s:
int twice(int x) {
int result = x + x;
return result;
}
Here's our conversion:
template <typename T>
T twice(const T& x) {
T result = x + x;
return result;
}
Notes
- The argument is a
const
reference, both so that we can avoid copying its contents and also make sure we can't change what was passed in. - The function
return
s by value because it will be returning a temporary value and we should never return a reference to a temporary value. - We avoided the
*
operator (e.g.2*x
), because we want the function to work with any type that supports+
, even if it doesn't support*
.
(When logged in, completion status appears here.)