5.2.14 · D4C++ Programming

Exercises — Templates — function templates, class templates

2,756 words13 min readBack to topic

Every answer that produces a number is machine-verified.


Level 1 — Recognition (can you read the syntax?)

L1·Q1 — Spot the template head

Recall Solution

Answer: B. The keyword is template, followed by an angle-bracket list of parameters, and each type parameter must be introduced by typename (or the equivalent class). So template<typename T> (or template<class T>).

  • A is wrong: T is used before it is declared — you cannot write <T>, you must say "T is a type": <typename T>.
  • C and D just scramble the keywords.

L1·Q2 — Count the instantiations

Recall Solution

Answer: 3. Instantiation is keyed by the concrete type, not by how many times you call it.

  • twice(3) and twice(4) both deduce T = intone int version, shared.
  • twice(2.5) deduces T = double → a second version.
  • twice<long>(9) forces T = long → a third version. Same type = same generated function reused. So the two int-calls collapse to one function, and adding the double and long versions gives three distinct functions total.

L1·Q3 — Which need explicit <...>?

Recall Solution
  • (i) compiles. You gave the type explicitly: Box<int>.
  • (ii) does NOT compile in C++14. A class template cannot deduce T from its constructor before C++17. You must write Box<int> b(5);. (In C++17+ Class Template Argument Deduction makes Box b(5) legal, but the question pins C++14.)

Level 2 — Application (make it work)

L2·Q4 — Fix the deduction conflict

Recall Solution

The failure: 3 makes deduction say T = int, 2.5 makes it say T = double. One T, two demands → conflict. Fix A — force the type explicitly:

auto r = maximum<double>(3, 2.5);  // 3 converts to 3.0, T = double

Fix B — two type parameters:

template<typename T1, typename T2>
auto maximum(T1 a, T2 b){ return (a > b) ? a : b; }
auto r = maximum(3, 2.5);

Value of r: compare 3 and 2.5; 3 is greater than 2.5, so the result is 3 (as a double, i.e. 3.0).

L2·Q5 — Non-type parameter size

Recall Solution
  • (a) N is baked in as 5, so a.size() returns 5.
  • (b) The only member is double data[5], i.e. 5 doubles at 8 bytes each: 5 times 8 is 40 bytes. So sizeof(a) == 40. N being a compile-time value (a non-type parameter, defined above) is exactly why the array lives on the stack with a fixed, known size — see Compile-time vs Runtime.

L2·Q6 — Write a generic swap

Recall Solution
template<typename T>
void swapValues(T& a, T& b){   // references, so the caller's variables change
    T tmp = a;                 // stash a
    a = b;                     // a becomes b
    b = tmp;                   // b becomes old a
}

Why T&? Without references we'd swap copies and the caller would see nothing. The & binds to the real variables. Trace: start x=1, y=9. tmp=1; x=9; y=1. Final: x == 9, y == 1.


Level 3 — Analysis (why does it behave this way?)

L3·Q7 — Linker error diagnosis

Recall Solution

Why it fails: A template is not code — it's a recipe. Code is generated only at a point of use. The point of use is in main.cpp. But main.cpp only sees the declaration from math.h; the body lives in math.cpp, a separate translation unit the main.cpp compiler never reads. With no body, the compiler cannot instantiate square<int>, so it emits a call to a function that is never defined → linker error. Fix: Put the full definition in the header (math.h). Then every file that includes it sees the whole body and can instantiate. Value: once fixed, square(4) returns 4 times 4, which is 16.

L3·Q8 — Which overload / specialization wins?

Recall Solution

Rule of thumb: a non-template exact match beats a template; among templates the most specialized wins.

  • (i) pick(5) → the int full specialization of the template exists and matches exactly → "int-special".
  • (ii) pick(2.0) → there is an ordinary (non-template) pick(double). A non-template function is preferred over any template when both match equally well → "plain-double".
  • (iii) pick('a') → no special version for char; the primary template instantiates with T = char"generic". See Template Specialization and Function Overloading for the full ranking rules.

Level 4 — Synthesis (build something new)

L4·Q9 — A generic Pair with a swap

Recall Solution
#include <type_traits>
 
template<typename A, typename B>
class Pair {
public:
    A first;
    B second;
    Pair(A a, B b) : first(a), second(b) {}
    void flip() {
        // hard compile-time guard: refuse unless A and B are the SAME type
        static_assert(std::is_same<A, B>::value,
                      "flip() requires both members to have the same type");
        A tmp = first;
        first  = second;
        second = tmp;
    }
};

Why static_assert(std::is_same<A,B>::value, ...)? Without it, flip() would silently compile for any two convertible types (say int and short), which is not what we want. The static_assert makes the compiler reject Pair<int,short>::flip() with a clear message, so only genuinely same-type pairs work. Trace: Pair<int,int> p{3,8}first=3, second=8. flip(): tmp=3; first=8; second=3. Result: p.first == 8, p.second == 3.

L4·Q10 — Specialize for a pointer-printing Printer

Recall Solution
template<typename T>
struct Printer<T*> {                  // partial specialization: any pointer
    static const char* kind(){ return "pointer"; }
};

The pattern Printer<T*> matches any pointer type and is more specialized than the primary template, so the compiler prefers it whenever the argument is a pointer.

  • Printer<int>::kind() → no *, primary template → "value".
  • Printer<int*>::kind() → matches T* with T = int"pointer".

Level 5 — Mastery (put it all together)

L5·Q11 — Generic fixed-size stack

Recall Solution
template<typename T, int N>
class Stack {
    T data[N];          // fixed storage, size known at compile time
    int top = 0;        // number of elements currently held
public:
    void push(T v){ data[top++] = v; }
    T    pop(){ return data[--top]; }
    int  count() const { return top; }
};

Trace top: start 0. push, push, push → 3. pop → 2. push, push → 4. So s.count() == 4. (Capacity N=4 is a non-type parameter baked in at compile time; the buffer lives on the stack.)

L5·Q12 — Compare macro vs template safety

Recall Solution
  • (a) Text substitution gives:
    int j = ((i++) > (3) ? (i++) : (3));
    i++ appears twice. It is evaluated once in the condition, and — because i++ returns 5 which is > 3 — the true branch runs i++ again. So i is incremented twice. This "double evaluation" is the classic macro trap.
  • (b) Start i = 5. Condition i++: uses 5, i becomes 6. Since 5 is greater than 3, the true branch i++ runs: uses 6, i becomes 7. Final i == 7 (and j == 6).
  • (c) A template is a real function, not text substitution. Its argument a is evaluated exactly once to initialize the parameter, and then only that stored parameter (a copy) is compared and returned — the caller's expression i++ is never re-run. So maxT(i++, 3) increments i a single time. This single-evaluation guarantee, together with full type-checking, is the core reason templates beat macros. See Macros vs Templates.

Recall One-line self-check

Templates deduce type from arguments (functions) but need it spelled out for classes pre-C++17 ::: True — function argument deduction vs no constructor deduction. A macro double-evaluates its arguments; a template evaluates each argument once ::: True — templates are real functions. Non-type template parameters must be compile-time constants ::: True.