NOTE:

These are a little sketchy...I'll fill in details 'soon'.

What Came Before

After we'd been coding for a while, we noticed things like these in our code:

    cout << "Enter length for the triangle's first side:  ";
    cin >> side1;
    cout << "Enter length for the triangle's second side:  ";
    cin >> side2;
    cout << "Enter length for the triangle's third side:  ";
    cin >> side3;

This redundancy is annoying and error prone, yet still we would often say at this stage, "Aw, I can just copy/paste/edit and all will be fine." Then we saw things like this:

    t = get_opt_pre('\0');
    if ( t == '\0' )
    {
        cout << "missing (";
    }
    else if ( t != '(' )
    {
        cout << "invalid:  need (";
    }
    cin >> x1;
    t = get_opt_pre('\0');
    if ( t == '\0' )
    {
        cout << "missing ,";
    }
    else if ( t != ',' )
    {
        cout << "invalid:  need ,";
    }
    cin >> y1;

And we thought, that's a bit more complicated to adjust. Maybe I should use a function:

    double get_numb_with_lead(char desired_leader, string missing_msg,
                              string invalid_msg);

    // in some function:
    {
        x1 = get_numb_with_lead('(', "Open parenthesis missing!\n",
                                     "Point must have a parenthesis before x-coordinate.\n");
        y1 = get_numb_with_lead(',', "Comma missing!\n",
                                     "Point must have a comma between coordinates.\n");
    }

    double get_numb_with_lead(char desired_leader, string missing_msg,
                              string invalid_msg)
    {
        double number;
        char t{get_opt_pre('\0')};
        if ( t == '\0' )
        {
            cout << missing_msg;
        }
        else if ( t != desired_leader )
        {
            cout << invalid_msg;
        }
        cin >> number;
        return number;
    }

Extracting the common data values into function arguments/parameters — be it for storage or mere access — proved much simpler in the long run.

Later we saw even weirder things like this:

    void swap(int & a, int & b)
    {
        int t{a};
        a = b;
        b = t;
        return;
    }
    void swap(short & a, short & b)
    {
        short t{a};
        a = b;
        b = t;
        return;
    }
    void swap(long & a, long & b)
    {
        long t{a};
        a = b;
        b = t;
        return;
    }
    void swap(float & a, float & b)
    {
        float t{a};
        a = b;
        b = t;
        return;
    }
    void swap(double & a, double & b)
    {
        double t{a};
        a = b;
        b = t;
        return;
    }
    void swap(char & a, char & b)
    {
        char t{a};
        a = b;
        b = t;
        return;
    }
    void swap(bool & a, bool & b)
    {
        bool t{a};
        a = b;
        b = t;
        return;
    }
    void swap(string & a, string & b)
    {
        string t{a};
        a = b;
        b = t;
        return;
    }

This isn't even as complicated as the first one, but it seems much more redundant — and it is already using functions! (Yes, they have to be separate functions, too. They are acting on different types of data and so the compiler needs to see them separately to know exactly how, for instance, = is to act here.)

This one, too:

bool remove(vector<string> & vec,
            vector<string>::size_type rem_me)
{
    vector<string>::size_type pos{rem_me-1};
    bool okay{pos < vec.size()};
    if (okay)
    {
        for (vector<string>::size_type k{pos+1}; k != vec.size(); ++k)
        {
            vec[k-1] = vec[k];
        }
        vec.pop_back();
    }
    return okay;
}
bool remove(vector<char> & vec,
            vector<char>::size_type rem_me)
{
    vector<char>::size_type pos{rem_me-1};
    bool okay{pos < vec.size()};
    if (okay)
    {
        for (vector<char>::size_type k{pos+1}; k != vec.size(); ++k)
        {
            vec[k-1] = vec[k];
        }
        vec.pop_back();
    }
    return okay;
}
bool remove(vector<double> & vec,
            vector<double>::size_type rem_me)
{
    vector<double>::size_type pos{rem_me-1};
    bool okay{pos < vec.size()};
    if (okay)
    {
        for (vector<double>::size_type k{pos+1}; k != vec.size(); ++k)
        {
            vec[k-1] = vec[k];
        }
        vec.pop_back();
    }
    return okay;
}

Sure we could get rid of the size_types inside the function with auto, but that just makes it even more annoying that we had to write three functions!

Or how about this from just ahead:

    cout << "Enter input file name:  ";
    getline(cin, fname);
    file.open(fname);
    while (!file)
    {
        file.close();
        file.clear();
        cout << "Could not open '" << fname
             << "'...\a\n\nEnter input file name:  ";
        getline(cin, fname);
        file.open(fname);
    }
    cout << "Enter output file name:  ";
    getline(cin, fname);
    file2.open(fname);
    while (!file2)
    {
        file2.close();
        file2.clear();
        cout << "Could not open '" << fname
             << "'...\a\n\nEnter output file name:  ";
        getline(cin, fname);
        file2.open(fname);
    }

And thought, "Hmm...first the values or variables changed between duplicated code. That we handled by making functions with arguments for the different values or variables. But now it isn't just values and variables — the variables' types are changing! How on earth can we handle that?!"

Well, lucky programmer, you are using C++! C++ supports generic programming via templates. With this powerful feature, you can write C++ code which is type-free and have the compiler fill in types as it generates the binary version of your program. Sound too good to be true? Follow along and see!

Making a template Function

Recognizing when to use a template is fairly simple (in general). Simply notice that you are repeating a lot of code and that you'd like to make it into a re-usable function. Then, when you can't because data types are different — even though the code itself is NO different — make the function anyway and change it to a template.

Let's practice with that swap function from above:

    void swap(int & a, int & b)
    {
        int t{a};
        a = b;
        b = t;
        return;
    }
    // ...etc...

All 7 of these functions are essentially the same. In fact, the code for swapping two values is never different. Note the code for swapping two Rational objects or two string objects:

    void swap(Rational & a, Rational & b)
    {
        Rational t{a};
        a = b;
        b = t;
        return;
    }
    void swap(string & a, string & b)
    {
        string t{a};
        a = b;
        b = t;
        return;
    }

Same thing! Only the types of a, b, and t have changed!

So, since we are performing the same code to variables/values which are merely differently typed, we know that we want to make a template, how do we do it? First we remove the offending type:

    void swap(_____ & a, _____ & b)
    {
        _____ t{a};
        a = b;
        b = t;
        return;
    }

Now we fill in the blank with a clearly identifying name. (Sure the five underscores would work, but it is a little tacky and confusing.) The book and many other authors use T, but we know this is a poor identifier in general. (Sure it is being used in a general function — look, we're using the generic names a, b, and t — but I'm trying to make a point that you can use proper, clear identifiers even for these template blanks!) I'll use SwapType since this is the type of information we'll be swap'ing:

    void swap(SwapType & a, SwapType & b)
    {
        SwapType t{a};
        a = b;
        b = t;
        return;
    }

But if the compiler saw that, it would complain saying, "What's this SwapType nonsense?! I've never heard of such a type!" (Or something to that effect...*grin*) So, we needed to make a way to identify the name of the new template function's blank to the compiler. Since this blank is to be filled in with a data type, we say:

    template <typename SwapType>
    void swap(SwapType & a, SwapType & b)
    {
        SwapType t{a};
        a = b;
        b = t;
        return;
    }

To indicate that the following function is covered by a template which uses the typename SwapType. Odd syntax, perhaps, but valid C++ code, nonetheless!

template Function Instantiation

Let's say that in our program we had some variables:

    short one, two;
    double three, four;
    Date five, six;
    string seven, eight;
    short nine, ten;

And we wanted to swap them pairwise. We would obviously code:

    swap(one, two);
    swap(four, three);
    swap(five, six);
    swap(eight, seven);
    swap(nine, ten);

(Note that it doesn't matter which is first or second in the list since the entire point of the function is to swap the values in those memory locations.)

Since the compiler only has only the one templated function swap, it will have to create binary versions — called instantiations — to place in the executable. To do that, it must validate the call — make sure that the function will work with the provided types. It will first make a checklist of all the things in the function that rely on the template type:

    template <typename SwapType>
    void swap(SwapType & a, SwapType & b)      // both arguments same
    {
        SwapType t{a};                                 // copy construction
        a = b;                                         // operator=
        b = t;
        return;
    }

That done, it will then check each call against this list to verify that all is well:

    swap(one, two);               // both short
                                  // can copy construct short
                                  // can assign short

Since all is well here, it creates a binary version by filling in all the blanks in the template with short. It calls this binary version swap<short>. Now, when it looks for a swap function to call, it has this list:

  1. swap<short>

  2.     template <typename SwapType>
        void swap(SwapType & a, SwapType & b)
        {
            SwapType t = a;
            a = b;
            b = t;
            return;
        }            // both args same type, copy constr, op=
    

If it ever sees another call with short as the template candidate, it will immediately use that version — avoiding the checklist process.

Before we do the other three calls, though, note how we validated this template. The compiler had to go through the function's definition to build its checklist of type necessities. What does that mean? It means the compiler had to see the entire function definition before a call to the function could happen.

Hmm... Sounds familiar... Oh, yeah! inline functions were the same way, weren't they? To call an inline function, you had to have the entire definition first. Otherwise it just made a regular call.

But it is actually much worse for templates. If the compiler doesn't have the definition before the call — it won't allow the call at all.

So, what does all that mean? It means you cannot effectively prototype a template function. The compiler must see the entire definition before the call is allowed. Even then the compiler must validate the call by testing its checklist.

Okay. Now a couple of terms and we'll continue checking our swap template. The process of validating the checklist and creating the binary version is called instantiation of the template. The funny name of the binary version is called the instantiation/instantiated name. Okay...back to the example...

Next the compiler sees this call:

    swap(four, three);

Before running through the checklist, though, it now has a choice. It first looks to see if there is a prior instantiation. Finding only swap<short>, it must re-validate the template for double:

    swap(four, three);      // both double
                            // can copy construct double
                            // can assign double

And thus swap<double> is generated and added to the list of swap functions available:

  1. swap<short>

  2. swap<double>

  3.     template <typename SwapType>
        void swap(SwapType & a, SwapType & b)
        {
            SwapType t = a;
            a = b;
            b = t;
            return;
        }            // both args same type, copy constr, op=
    

Next it sees the call:

    swap(five, six);

Seeing no pre-instantiated possibility with a Date type, it must validate the template for Date:

    swap(five, six);      // both Date
                          // can copy construct Date
                          // can assign Date

All being well, we create swap<Date> and add it to the list of swap functions available. Next up is:

    swap(eight, seven);

We look through the list for a string version of swap. Seeing none, we proceed to our checklist:

    swap(eight, seven);      // both string
                             // can copy construct string
                             // can assign string

An thus swap<string> is born and placed in our pool of swaps.

Next we see:

    swap(nine, ten);

This one is pretty easy as we simply find swap<short> in our list and use it! No validation needed since it was done already!

Let's pause again and look at what's been happening. How many swap functions does the compiler have now? Five, right: one blank and four binary instantiations. How many versions of swap are in the executable? Four: the four instantiations. Now for the kicker: How many swap functions did we write? One: the template. That's pretty sweet. We write it once and it is re-used five times (twice for short, remember)! That's pretty powerful!

Caveat

One potential problem especially related to vectors is that of nested types from inside a class. For example, on our remove functions from above there were references to the size_type of the position to be removed:

bool remove(vector<double> & vec,
            vector<double>::size_type rem_me)
{
    auto pos{rem_me-1};
    bool okay{pos < vec.size()};
    if (okay)
    {
        for (auto k{pos+1}; k != vec.size(); ++k)
        {
            vec[k-1] = vec[k];
        }
        vec.pop_back();
    }
    return okay;
}

If we made our template too naively, the compiler wouldn't be able to tell if that size_type was a nested type or constant from the class! To alleviate this, we use the typename keyword again to point out to the compiler that this size_type entity is indeed the name of a type:

template <typename Base_Type>
bool remove(vector<Base_Type> & vec,
            typename vector<Base_Type>::size_type rem_me)
{
    auto pos{rem_me-1};
    bool okay{pos < vec.size()};
    if (okay)
    {
        for (auto k{pos+1}; k != vec.size(); ++k)
        {
            vec[k-1] = vec[k];
        }
        vec.pop_back();
    }
    return okay;
}

If this usage seems a bit dense or verbose, you can do it another way:

template <typename Container_Type>
using Size_Type = typename Container_Type::size_type;

template <typename Base_Type>
bool remove(vector<Base_Type> & vec,
            Size_Type<vector<Base_Type>> rem_me)
{
    auto pos{rem_me-1};
    bool okay{pos < vec.size()};
    if (okay)
    {
        for (auto k{pos+1}; k != vec.size(); ++k)
        {
            vec[k-1] = vec[k];
        }
        vec.pop_back();
    }
    return okay;
}

Here the Size_Type using alias binds the typename to the nested size_type for our chosen container — no matter what kind of container it is. This keeps things generic but more readable. At least that is some people's opinion...

It does have the advantage that the Size_Type alias only has to be made once and can then be used on many further function templates. *shrug*

Also See...

Examples from lecture...

Other examples from lecture...