5.2.14 · D3C++ Programming

Worked examples — Templates — function templates, class templates

2,347 words11 min readBack to topic

Prerequisites you may want open: Function Overloading, Type Deduction (auto), Compile-time vs Runtime, Template Specialization, Macros vs Templates, Generic Programming, STL Containers.


The scenario matrix

Templates don't have "quadrants" like an angle problem — but they do have a fixed set of situations that behave differently. Here is every cell we must cover.

# Case class The question it tests Covered by
A Function template, clean deduction Compiler reads args, finds ONE T Ex 1
B Deduction conflict (degenerate input) Two arg types → no single T Ex 2
C Explicit type argument You override deduction with <...> Ex 3
D Class template, explicit <T> No constructor deduction (pre-C++17) Ex 4
E Non-type parameter (compile-time value) A number, not a type, in the <> Ex 5
F Full specialization (the "wrong for one type" case) Override the generic recipe Ex 6
G The linker trap (definition in .cpp) Real-world debugging word problem Ex 7
H Exam twist: overload vs template resolution Which function actually gets called? Ex 8

Each example below is tagged with its cell letter.


Ex 1 — Cell A: function template, clean deduction

Forecast: Guess T for each call and the returned value before reading on.

  1. Look only at the argument types. 3 and 7 are both int. Why this step? Deduction ignores the values — it matches the types of the arguments to the pattern T a, T b. Both slots are int, so there is exactly one candidate: T = int.
  2. Instantiate. The compiler stamps out int maximum(int, int). Why? Only now does real machine code exist — an unused template compiles to nothing (Compile-time vs Runtime).
  3. Evaluate the body. (3 > 7) ? 3 : 77. Why? The ternary picks the larger.
  4. Second call: 2.5, 1.1 are both doubleT = double, returns (2.5 > 1.1) ? 2.5 : 1.1 = 2.5.

Verify: maximum(3,7) gives 7 (an int); maximum(2.5,1.1) gives 2.5 (a double). Sanity check: the answer is always one of the two inputs, and it's the bigger one. ✓


Ex 2 — Cell B: deduction conflict (degenerate input)

Forecast: Does it return 3, 3.0, or refuse to compile?

  1. Match arg 1 to the pattern: 3 is intT = int. Why? Same deduction rule as before, applied to the first slot.
  2. Match arg 2 to the pattern: 2.5 is doubleT = double. Why? The second slot is also T, so it demands T = double.
  3. Reconcile. T must be one type, but step 1 says int and step 2 says double. Contradiction → compile error (not a runtime error). Why this step? Template argument deduction never inserts conversions; it needs an exact type match in every slot that uses T.
  4. The fix — force one T: maximum<double>(3, 2.5). Now T = double is fixed, and 3 converts to 3.0 on the way in. Returns 3.0. Why the conversion is now allowed? Once T is pinned by hand, the args are just being passed to a known double parameter — ordinary implicit conversion applies.

Verify: maximum(3, 2.5) → does not compile. maximum<double>(3, 2.5)3.0, a double. Sanity: 3.0 > 2.5, so the larger, 3.0, is returned. ✓


Ex 3 — Cell C: explicit type argument that changes the answer

Forecast: Are both 3.5? Or does one truncate?

  1. half(7): deduce T = int (arg is int). Why? Clean single-type deduction (Cell A machinery).
  2. Body with T = int: 7 / 2 is integer division3. Why this step? 2 written in the body is an int; int / int in C++ discards the remainder. The template didn't change arithmetic rules — it just chose int.
  3. half<double>(7): T forced to double; the 7 converts to 7.0. Why? Explicit <double> overrides deduction, exactly like Ex 2's fix.
  4. Body with T = double: 7.0 / 23.5. Why? double / int promotes 2 to 2.0, so real division happens.

Verify: half(7) == 3 (integer division), half<double>(7) == 3.5. The two differ because of the chosen T, not the algorithm. ✓


Ex 4 — Cell D: class template needs explicit <T>

Forecast: Does the constructor's 42 tell the compiler T = int the way maximum(42) would?

  1. Recall the difference: function templates deduce T from arguments; class templates (before C++17) do not deduce from a constructor call. Why? A function is the thing you call, so its args are visible at the call. A class name Box must be a complete type before any constructor is considered — the type has to be known first. So you must name it: Box<int>.
  2. Write Box<int> b(42);. The compiler generates a concrete class Box<int> where every T becomes int. Why? Instantiation replaces the parameter throughout the class body.
  3. Call b.get(): returns the stored value, which is 42.
  4. Note the type identity: Box<int> and Box<double> are unrelated types — you cannot assign one to the other. This is the same rule that makes vector<int> and vector<double> different (STL Containers).

Verify: Box<int> b(42); b.get()42. Box b(42); fails to compile pre-C++17. ✓


Ex 5 — Cell E: non-type template parameter

Forecast: Is N a variable you can change at runtime, or is it baked in?

Figure — Templates — function templates, class templates
  1. Identify the parameters: T is a type, N is a value — specifically a compile-time int (Compile-time vs Runtime). Why this step? Templates accept two flavours of parameter. N is not a type, so it fills the int N slot with an actual number. Look at the figure: the violet <double, 5> fills both slots of the cutter — one with a type, one with a number.
  2. Instantiate Array<double, 5>. Now data is literally double data[5] — a fixed-size array, size known before the program runs. Why it matters? Because 5 is a compile-time constant, the array sits on the stack, no heap allocation, and the compiler can inline everything.
  3. Call a.size(): returns N, which the compiler already replaced with 5. This is a compile-time constant, effectively return 5;.
  4. Degenerate check — could N be 0? Array<double, 0> is legal-ish but a zero-length array is a corner case (technically ill-formed for a plain T data[0]); avoid it. This is the "zero input" cell of our matrix.

Verify: a.size() == 5, storage is double[5] on the stack (5 * 8 = 40 bytes on a typical 8-byte-double platform). ✓


Ex 6 — Cell F: full specialization (generic recipe is wrong for one type)

Forecast: Do both print "1"? Or does bool behave differently?

Figure — Templates — function templates, class templates
  1. Printer<int>::show(1) uses the generic template: to_string(1)"1". Why this step? There is no special version for int, so the compiler falls back to the general cookie-cutter.
  2. Printer<bool>::show(true) — the compiler sees the exact-match specialization Printer<bool> and prefers it over the generic one. Why? The most specialized matching template wins (Template Specialization). In the figure, bool follows the orange arrow to its own dedicated recipe.
  3. Run the specialized body: true ? "true" : "false""true". Why we specialized? The generic to_string(true) would give "1" — technically correct but not the human-readable word we wanted. Specialization fixes the one type that misbehaves without touching the others.

Verify: Printer<int>::show(1) == "1", Printer<bool>::show(true) == "true", Printer<bool>::show(false) == "false". ✓


Ex 7 — Cell G: the linker trap (real-world word problem)

Forecast: Is this a syntax bug, a runtime crash, or a linker problem? Guess before reading.

  1. Where is Stack<int> first used? In main.cpp. Why this step? Instantiation happens at the point of use. The compiler compiling main.cpp is the one that must generate Stack<int>::push.
  2. What can the main.cpp compiler see? Only stack.h (the declaration). The body of push lives in stack.cpp, a separate translation unit (Compile-time vs Runtime). Why it matters? Without the body, the compiler emits a call to Stack<int>::push but never generates the function itself — it assumes some other unit will.
  3. stack.cpp never instantiates Stack<int> (it never mentions int), so nobody generates the code → the linker finds a dangling reference → undefined reference. Why "linker" and not "compiler"? Each file compiled fine on its own; the mismatch only shows when linking the object files together.
  4. The cure: put the full template definition in the header (.h/.hpp), so every file that uses the template sees the body and can instantiate it. Why this is safe from multiple-definition errors? Template instantiations get special linkage — identical instantiations across units are merged, not duplicated.

Verify (conceptual): move the body into stack.hmain.cpp sees it → Stack<int>::push is generated → link succeeds. The class of bug is a linker error, confirmed by the undefined reference wording. ✓


Ex 8 — Cell H: exam twist — overload vs template resolution

Forecast: Three calls, three answers. Guess each.

  1. who(5): an exact non-template match who(int) exists. Why this step? When a normal (non-template) function is an equally good match, the compiler prefers the non-template overload (Function Overloading). So it returns "plain int".
  2. who(2.5): the plain who(int) would require a double → int conversion (a worse match), but who(T) with T = double is an exact match. Why? The template gives an exact type match; exact beats a conversion. Returns "template".
  3. who<int>(5): the <int> explicitly demands the template with T = int, so the plain overload is not even considered as the primary — the template wins. Returns "template". Why? You named a template argument, which only a template can accept, so overload resolution is forced onto the template path.

Verify: who(5) == "plain int", who(2.5) == "template", who<int>(5) == "template". Sanity: exact-match plain function beats template for 5; template wins for 2.5 (avoids a narrowing conversion) and whenever you write <...>. ✓


Recall Quick self-test

Why does maximum(3, 2.5) fail? ::: Deduction gives T=int from arg 1 and T=double from arg 2 — one T can't be both. half(7) returns? ::: 3, because T=int makes 7/2 integer division. half<double>(7) returns? ::: 3.5, because T=double makes it real division. Why must Box b(42); be Box<int> b(42); pre-C++17? ::: Class templates don't deduce T from a constructor; the type must be named first. Printer<bool>::show(true) returns? ::: "true", from the full specialization that overrides the generic recipe. The linker error undefined reference for a template means? ::: The definition sat in a .cpp the using file couldn't see; put the full body in the header. who(2.5) returns? ::: "template" — exact template match beats a double→int conversion into the plain overload.