CS 70

Function Templates

  • LHS Cow speaking

    Let's prevent all that code duplication!

  • Pig speaking

    By learning MORE C++ features??!!

  • LHS Cow speaking

    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).

  • Horse speaking

    Hay! What if I have a class Unknowable that doesn't support <<? Can I still make a printVal<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.

  • Hedgehog speaking

    Huh. Let me try… AAAAUUGH! The compiler just printed like seven pages of errors!!

  • LHS Cow speaking

    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!

  • Hedgehog speaking

    Sometimes there are so many errors that the first one has scrolled off the screen. What do I do then?

  • LHS Cow speaking

    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

  • Dog speaking

    This is cool! What about fill()? Could we do that?

  • LHS Cow speaking

    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 ints:

int twice(int x) {
    int result = x + x;
    return result;
}

Convert this function into a function template, so it can return double the value of a variable of any type that works with the + operator.

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 returns 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.)