5.2.14 · D2C++ Programming

Visual walkthrough — Templates — function templates, class templates

2,070 words9 min readBack to topic

This is a companion to the main Templates note. It leans on ideas from Compile-time vs Runtime, Function Overloading, and Type Deduction (auto). Every word below is built from zero — you need to know nothing but "a function takes inputs and returns an output."


Step 1 — Start from ONE concrete function

WHAT. We write the simplest possible function that returns the larger of two whole numbers.

int maximum(int a, int b) { return (a > b) ? a : b; }

WHY start here. You cannot generalize something you have not first made concrete. Look at the body: (a > b) ? a : b means "if a is bigger than b, hand back a, otherwise hand back b." The little ?: is called the ternary operator — read it as a one-line "if/else that produces a value." Notice what the body actually needs: it needs to compare two things with >, and it needs to copy one back out. It never uses anything that is special to int.

PICTURE. The function is a box. Two int values go in the left; one int comes out the right. The word int appears three times — coloured red — and every one of those reds is a promise that says "only whole numbers allowed here."

Figure — Templates — function templates, class templates

Step 2 — Notice the type is the only thing that changes

WHAT. Suppose you now need the maximum of two doubles (decimals). You copy-paste and swap every int for double:

double maximum(double a, double b) { return (a > b) ? a : b; }

WHY show this. Put the two functions side by side. The bodies are byte-for-byte identical. The only difference is the three type labels. This is the whole motivation for templates: the duplicated part (the logic) should be written once; the varying part (the type) should become a knob you can turn.

PICTURE. Two boxes stacked. Their insides — the (a>b)?a:b gear — are the same green gear. Only the input/output labels differ (int red on top, double orange below). The green gear is what we want to keep; the coloured labels are what we want to make swappable.

Figure — Templates — function templates, class templates

Step 3 — Replace the type with a placeholder name T

WHAT. Instead of writing a fixed type, we invent a placeholder and call it T. We tell the compiler "T is a type I'll fill in later" with a header line, and then use T wherever the type went.

template<typename T>        // "T stands for some type — to be chosen"
T maximum(T a, T b) {       // the same three labels, now all T
    return (a > b) ? a : b; // the green gear, untouched
}

WHY this exact syntax. template<typename T> is the compiler's word for "what follows is a blueprint, and T is its blank." The word typename here just means "T names a type." (The parent note tells you typename and class are interchangeable here — same meaning.) The blueprint is not code yet — it is a shape with a hole in it labelled T.

PICTURE. The same box as Step 1, but the three red int labels are replaced by three blue T labels, and a new banner template<typename T> sits on top like a nameplate. A dashed grey outline around the whole thing signals "this is a blueprint, not a real function — no machine code exists yet."

Figure — Templates — function templates, class templates

Step 4 — The compiler fills the hole: instantiation

WHAT. You call the blueprint with real arguments:

maximum(3, 7);      // both arguments are int

The compiler looks at the arguments 3 and 7, sees they are int, and fills the T hole with int. Only now does real code appear.

WHY the compiler can do this alone. This is type deduction (same machinery behind auto): the compiler reads the arguments' types and solves for T. Here 3 → int and 7 → int, so T = int. It then substitutes int for every T and generates:

int maximum(int a, int b) { return (a > b) ? a : b; }

— which is exactly the function we hand-wrote in Step 1. We got it for free.

PICTURE. The blue-T blueprint on the left. An arrow labelled T = int flows right into a solid (no longer dashed) red box — a real, callable int function. The arrow is the act of instantiation; the solid box is the stamped-out cookie.

Figure — Templates — function templates, class templates

Step 5 — One blueprint, many cookies (and none for unused types)

WHAT. Now call it with more types:

maximum(3, 7);          // T = int    -> stamps an int version
maximum(2.5, 1.1);      // T = double -> stamps a double version
maximum<char>('a','z'); // T = char, forced explicitly

WHY the third one looks different. maximum<char>(...) puts the type inside the angle brackets by hand instead of letting the compiler guess. You do this when you want to force a type. Everything else deduces automatically.

The crucial edge fact: a type you never call produces zero code. If your program never does maximum on a string, no string maximum exists in the final program. The blueprint is "pay only for what you use."

PICTURE. One blue blueprint in the centre. Three solid boxes fan out from it — red int, orange double, green char — each labelled with its T = .... A greyed-out, crossed-through ghost box labelled string (never called) sits off to the side to show it does not get made.

Figure — Templates — function templates, class templates
Recall

If a program calls maximum only on int and double, how many real functions get generated? ::: Exactly two — one for int, one for double. Unused types generate nothing.


Step 6 — The degenerate case: conflicting deductions

WHAT. Try mixing types in one call:

maximum(3, 2.5);   // 3 is int, 2.5 is double

This fails to compile.

WHY it fails. Deduction must produce one single T. The first argument argues "T should be int"; the second argues "T should be double." The compiler cannot obey both, so it refuses — it will not silently pick one for you. This is a feature: it stops you from a hidden, possibly wrong, conversion.

Two fixes:

maximum<double>(3, 2.5);   // force T = double; the int 3 converts to 3.0

or make the two parameters independent types:

template<typename T1, typename T2>
auto maximum(T1 a, T2 b) { return (a > b) ? a : b; }  // C++14 deduces the return type

PICTURE. The blueprint with its single T hole. Two arrows point into that one hole: a red one carrying int, an orange one carrying double. They collide at the hole and a red ✗ flags the conflict — "one hole, two incompatible demands."

Figure — Templates — function templates, class templates

Step 7 — Same machine for classes: the Box<T> blueprint

WHAT. The identical idea applies to whole classes. A class bundles data + functions. We make one whose stored type is a hole:

template<typename T>
class Box {
    T value;                       // the stored thing is of the hole type T
public:
    Box(T v) : value(v) {}         // build a Box from a T
    T get() const { return value; }
};

WHY you must say Box<int>, not just Box. A function template can look at its arguments to deduce T. A class template (before C++17) has nothing to look at when you first name the type, so you fill the hole yourself:

Box<int>    bi(42);     // instantiates a class Box<int>
Box<string> bs("hi");   // instantiates a SEPARATE class Box<string>

Box<int> and Box<string> are different types — as unrelated as int and string. They share no runtime code. This is exactly how vector<T> works.

PICTURE. One blue Box<T> blueprint holding a T-shaped slot. Two solid class boxes stamped from it: a red Box<int> holding 42, and a green Box<string> holding "hi". A wall between them labelled "unrelated types — no shared code."

Figure — Templates — function templates, class templates

The one-picture summary

WHAT the whole page said, in one frame. Concrete function → notice only the type varies → punch a T hole to make a blueprint → the compiler deduces or is told T → it stamps out one real cookie per distinct type used, and nothing for unused types; mixing types in one hole is the one degenerate case that breaks deduction.

Figure — Templates — function templates, class templates
Recall Feynman: retell the whole walkthrough in plain words

I started with a machine that only chews on whole numbers. I noticed that if I wanted it to chew decimals, I'd copy the whole machine and just repaint the "whole numbers" signs to say "decimals" — the guts never changed. So I peeled off the signs entirely and hung a blank sign that says "T — some type, TBD," and I stuck a nameplate template<typename T> on the front. That blank machine isn't real yet; it's a drawing of a machine. Then, the moment I actually feed it two whole numbers, the factory (the compiler) reads the numbers, writes int on all the blanks, and welds together a real whole-number machine on the spot. Feed it decimals somewhere else and it welds a second, separate decimal machine. If I never feed it text, no text machine is ever built — I only pay for what I use. The one thing that jams the factory is handing the same blank a whole number in one hand and a decimal in the other: the blank can only become one type, so the factory stops and asks me to decide. Classes work the same way, except I have to tell the factory the type up front by writing Box<int>, because a class has no arguments to peek at.