Command Palette

Search for a command to run...

MTH 4300

Lecture 14

Why Templates?

C++ code written for one type often applies to many types. Without templates, you'd need a separate version of every function or class for int, double, String, and so on:

Without templates — verbose
int    max_int(int a, int b)          { return a > b ? a : b; }
double max_double(double a, double b) { return a > b ? a : b; }
// ...repeated for every type

Templates let you write the logic once and let the compiler generate the right version for each type it's used with.

Function Templates

A function template is parameterized on one or more types. Use the template keyword followed by the type parameter list:

Function template
template <typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

T is a placeholder. The compiler substitutes the real type when the function is called:

Using a function template
max(3, 5)      // T = int   — compiler generates int version
max(1.5, 2.7)  // T = double — compiler generates double version
max('a', 'z')  // T = char  — compiler generates char version

You can also name the type explicitly:

Explicit instantiation
max<int>(3, 5) // same as max(3, 5) — T forced to int

Type Deduction

When you call a function template without naming T, the compiler deduces it from the argument types. Deduction fails if the arguments disagree:

Type deduction
max(3, 5)          // ok — both int, T = int
max(3, 5.0)        // error — int vs double; T can't be both
max<double>(3, 5)  // ok — explicit T = double; int 3 converts to double

Template type deduction does not perform implicit conversions. If your arguments differ in type, either cast one or specify T explicitly.

Class Templates

A class can also be parameterized on a type. Unlike function templates, class templates always require you to name the type argument at the point of use:

Class template
template <typename T>
class Pair {
public:
    Pair(const T& first, const T& second)
        : first_(first), second_(second) {}

    const T& first()  const { return first_; }
    const T& second() const { return second_; }

private:
    T first_;
    T second_;
};
Using a class template
Pair<int>    p1(1, 2);
Pair<double> p2(3.14, 2.71);

The compiler generates a separate class for each distinct type argument. Pair<int> and Pair<double> are entirely independent types.

typename vs class

Inside a template parameter list, typename and class mean the same thing:

typename vs class — identical
template <typename T> T max(T a, T b); // preferred
template <class    T> T max(T a, T b); // same meaning

Prefer typename — it signals "any type" without implying the argument must be a class. class in this context is a historical artifact from early C++.

Stack<T> — A Generic Stack

We now apply class templates to build Stack<T>: a fixed-capacity last-in, first-out container.

Stack<T>
template <typename T>
class Stack {
public:
    Stack() : top_(0) {}

    void push(const T& value) {
        data_[top_++] = value;
    }

    T pop() {
        return data_[--top_];
    }

    const T& top() const {
        return data_[top_ - 1];
    }

    bool empty() const { return top_ == 0; }
    int  size()  const { return top_; }

private:
    static const int CAPACITY = 64;
    T   data_[CAPACITY];
    int top_;
};

Stack<T> works with any type that supports copy assignment — including String:

Stack<int> and Stack<String>
#include <iostream>

int main() {
    Stack<int> ints;
    ints.push(10);
    ints.push(20);
    ints.push(30);
    std::cout << ints.pop() << "\n"; // 30
    std::cout << ints.top() << "\n"; // 20

    Stack<String> words;
    words.push(String("hello"));
    words.push(String("world"));
    std::cout << words.pop() << "\n"; // world
    std::cout << words.top() << "\n"; // hello

    return 0;
}

No changes to String are required — the template machinery handles the rest.

Exercises

Q1.

Without running the code, predict what this program prints.

#include <iostream>

template <typename T>
T add(T a, T b) {
    return a + b;
}

template <typename T>
void print_twice(T value) {
    std::cout << value << " " << value << "\n";
}

int main() {
    std::cout << add(3, 4)           << "\n"; // line A
    std::cout << add(1.5, 2.5)       << "\n"; // line B
    print_twice(7);                            // line C
    print_twice(String("hi"));                 // line D
    return 0;
}

Q2.

Without running the code, predict what this program prints.

#include <iostream>

int main() {
    Stack<int> s;

    s.push(5);
    s.push(10);
    s.push(15);

    std::cout << s.top()   << "\n"; // line A
    std::cout << s.size()  << "\n"; // line B
    s.pop();
    std::cout << s.top()   << "\n"; // line C
    s.pop();
    s.pop();
    std::cout << s.empty() << "\n"; // line D

    return 0;
}

Q3.

Each snippet has a bug related to templates. Identify what's wrong.

// Snippet A — mismatched argument types
template <typename T>
T max(T a, T b) { return a > b ? a : b; }

int x = max(3, 4.5); // int and double

// Snippet B — missing type argument on class template
Stack s;
s.push(1);

// Snippet C — template assumes operator< exists
template <typename T>
T min(T a, T b) { return a < b ? a : b; }

struct Opaque { int val; }; // no operator<
Opaque p{1}, q{2};
Opaque r = min(p, q);

// Snippet D — arguments of different types deduced to same T
template <typename T>
void show(T a, T b) { std::cout << a + b << "\n"; }

show(String("foo"), 42);

Q4.

Write a function template clamp<T> that returns lo if value < lo, hi if value > hi, or value otherwise. Then write print_clamped, which prints the clamped value of each element in a plain int array.

#include <iostream>

template <typename T>
T clamp(T value, T lo, T hi) {
    // TODO
}

void print_clamped(int* arr, int n, int lo, int hi) {
    // TODO: print clamp(arr[i], lo, hi) for each element, space-separated
}

int main() {
    int data[] = {-5, 0, 3, 7, 12};
    print_clamped(data, 5, 0, 10);
    // expected: 0 0 3 7 10
    return 0;
}

Q5.

Implement the Stack<T> class template shown in this lecture. Then instantiate it with String to confirm it works with a non-trivial type.

#include <iostream>

template <typename T>
class Stack {
public:
    Stack() : top_(0) {}

    void     push(const T& value); // add to top
    T        pop();                // remove and return top element
    const T& top() const;          // inspect top without removing
    bool     empty() const;
    int      size()  const;

private:
    static const int CAPACITY = 64;
    T   data_[CAPACITY];
    int top_;
};

// TODO: implement all methods

int main() {
    Stack<int> ints;
    ints.push(1);
    ints.push(2);
    ints.push(3);
    std::cout << ints.pop() << "\n"; // 3
    std::cout << ints.top() << "\n"; // 2

    Stack<String> words;
    words.push(String("hello"));
    words.push(String("world"));
    std::cout << words.pop() << "\n"; // world
    std::cout << words.top() << "\n"; // hello

    return 0;
}

Practice Problems