Skip to content
C++ Better Explained
Go back

C++ Templates From Scratch: Generic Programming Explained Simply

Edit page

C++ Templates From Scratch: Generic Programming Explained Simply

Introduction: What Problem Do Templates Solve?

Templates are C++‘s most powerful feature, yet they often intimidate newcomers. Here’s the core idea: templates enable you to write code once and apply it to many different types without sacrificing type safety.

Imagine you want to implement a stack. A stack of integers works the same way as a stack of strings, which works the same way as a stack of custom objects. Without templates, you’d need to copy your implementation for each type. With templates, you write once and instantiate for many types.

Templates solve the fundamental tension in programming:

Templates give you both.

In this guide, we’ll build a practical understanding of C++ templates, from basic function templates to advanced metaprogramming, with clear examples at each step.

The “Before Templates” World: Function Overloading Limitations

Before templates existed, programmers used function overloading. Let’s see why this doesn’t scale:

// Function overloading - requires separate implementation for each type
int getMax(int a, int b) {
    return (a > b) ? a : b;
}

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

float getMax(float a, float b) {
    return (a > b) ? a : b;
}

string getMax(string a, string b) {
    return (a > b) ? a : b;
}

// Usage
int main() {
    cout << getMax(5, 10) << "\n";           // Calls int version
    cout << getMax(3.14, 2.71) << "\n";      // Calls double version
    cout << getMax(string("apple"), string("banana")) << "\n";  // Calls string version

    return 0;
}

Problems:

Templates elegantly solve this problem.

Function Templates: The Simplest Templates

A function template is a blueprint for functions. The compiler instantiates (creates) the actual function for each type you use it with.

// Function template - write once, use for any type!
template<typename T>
T getMax(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    // Compiler creates int version: int getMax<int>(int, int)
    cout << getMax(5, 10) << "\n";

    // Compiler creates double version: double getMax<double>(double, double)
    cout << getMax(3.14, 2.71) << "\n";

    // Compiler creates string version: string getMax<string>(string, string)
    cout << getMax(string("apple"), string("banana")) << "\n";

    // Explicit template argument (usually not needed)
    cout << getMax<float>(1.5f, 2.5f) << "\n";

    return 0;
}

Output:

10
3.14
banana
2.5

The beauty: one template definition, multiple type implementations. The compiler handles it automatically.

Multiple Template Parameters

Templates can have multiple type parameters:

// Two type parameters
template<typename T, typename U>
T findInVector(const vector<U>& vec, const U& target) {
    for (int i = 0; i < vec.size(); i++) {
        if (vec[i] == target) {
            return static_cast<T>(i);  // Return position
        }
    }
    return -1;  // Not found
}

int main() {
    vector<string> names = {"Alice", "Bob", "Charlie"};

    // Find index of "Bob" in vector of strings
    int pos = findInVector<int>(names, "Bob");
    cout << "Found at position: " << pos << "\n";

    return 0;
}

Output:

Found at position: 1

How Template Type Deduction Works

In most cases, you don’t need to specify template arguments explicitly. The compiler deduces them from the arguments you pass:

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

int main() {
    print(42);              // T deduced as int
    print(3.14);            // T deduced as double
    print("hello");         // T deduced as const char*
    print(string("world")); // T deduced as string

    return 0;
}

Type Deduction Rules (Simplified)

  1. Exact match preferred: If the argument exactly matches a type, use it
  2. Promotion allowed: int → double, char → int
  3. Qualification adjustment: T can deduce const T or T&
template<typename T>
void demonstrate(T value) {
    // T's type is deduced from value
}

int main() {
    int x = 5;
    demonstrate(x);         // T = int
    demonstrate(&x);        // T = int*
    demonstrate(string("hi")); // T = string

    // const and references affect deduction
    const int cx = 10;
    demonstrate(cx);        // T = const int (const preserved!)

    return 0;
}

Class Templates: Generic Data Structures

Class templates work the same way as function templates—they’re blueprints for classes.

// Generic stack template
template<typename T>
class Stack {
private:
    vector<T> elements;

public:
    void push(const T& value) {
        elements.push_back(value);
    }

    T pop() {
        if (elements.empty()) {
            throw runtime_error("Stack is empty");
        }
        T value = elements.back();
        elements.pop_back();
        return value;
    }

    bool isEmpty() const {
        return elements.empty();
    }

    int size() const {
        return elements.size();
    }
};

int main() {
    // Stack of integers
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    intStack.push(30);

    while (!intStack.isEmpty()) {
        cout << intStack.pop() << " ";  // Prints: 30 20 10
    }
    cout << "\n";

    // Stack of strings
    Stack<string> stringStack;
    stringStack.push("one");
    stringStack.push("two");
    stringStack.push("three");

    while (!stringStack.isEmpty()) {
        cout << stringStack.pop() << " ";  // Prints: three two one
    }
    cout << "\n";

    return 0;
}

Output:

30 20 10
three two one

Template Methods in Template Classes

template<typename T>
class Container {
private:
    vector<T> data;

public:
    // Template method - generic within a generic class
    template<typename U>
    void add(const U& value) {
        // Convert U to T if possible
        data.push_back(static_cast<T>(value));
    }

    void print() const {
        for (const auto& item : data) {
            cout << item << " ";
        }
        cout << "\n";
    }
};

int main() {
    Container<double> nums;
    nums.add(42);           // int → double
    nums.add(3.14f);        // float → double
    nums.add(2.718);        // double → double
    nums.print();           // 42 3.14 2.718

    return 0;
}

Template Specialization: Customizing for Specific Types

Sometimes you need different behavior for specific types. That’s where template specialization comes in.

Full Specialization

// Generic template
template<typename T>
class Printer {
public:
    void print(const T& value) {
        cout << value << "\n";
    }
};

// Full specialization for bool
template<>
class Printer<bool> {
public:
    void print(const bool& value) {
        cout << (value ? "true" : "false") << "\n";
    }
};

// Full specialization for vector<T>
template<typename T>
class Printer<vector<T>> {
public:
    void print(const vector<T>& vec) {
        cout << "Vector: [";
        for (size_t i = 0; i < vec.size(); i++) {
            cout << vec[i];
            if (i < vec.size() - 1) cout << ", ";
        }
        cout << "]\n";
    }
};

int main() {
    Printer<int> intPrinter;
    intPrinter.print(42);

    Printer<bool> boolPrinter;
    boolPrinter.print(true);

    Printer<vector<int>> vecPrinter;
    vecPrinter.print({1, 2, 3, 4, 5});

    return 0;
}

Output:

42
true
Vector: [1, 2, 3, 4, 5]

Partial Specialization

Partial specialization lets you specialize templates for categories of types:

// Generic template
template<typename T>
class Container {
public:
    void info() {
        cout << "Generic container\n";
    }
};

// Partial specialization for pointers
template<typename T>
class Container<T*> {
public:
    void info() {
        cout << "Container of pointers\n";
    }
};

// Partial specialization for vector
template<typename T>
class Container<vector<T>> {
public:
    void info() {
        cout << "Container of vectors\n";
    }
};

int main() {
    Container<int> c1;
    c1.info();  // Generic container

    Container<int*> c2;
    c2.info();  // Container of pointers

    Container<vector<int>> c3;
    c3.info();  // Container of vectors

    return 0;
}

Output:

Generic container
Container of pointers
Container of vectors

Default Template Parameters

Templates can have default parameters, just like functions:

template<typename T = int, typename U = double>
class Pair {
private:
    T first;
    U second;

public:
    Pair(T f, U s) : first(f), second(s) {}

    void print() const {
        cout << "(" << first << ", " << second << ")\n";
    }
};

int main() {
    Pair<> p1(5, 3.14);        // Uses defaults: int, double
    p1.print();

    Pair<int, int> p2(1, 2);   // Override both
    p2.print();

    Pair<string> p3("hello", 2.71);  // Override first, use default second
    p3.print();

    return 0;
}

Output:

(5, 3.14)
(1, 2)
(hello, 2.71)

Variadic Templates: Parameter Packs

Variadic templates (C++11+) let templates accept a variable number of arguments:

// Base case - when no arguments left
template<typename T>
T sum(T value) {
    return value;
}

// Recursive case - processes first argument, delegates rest
template<typename T, typename... Args>
T sum(T first, Args... rest) {
    return first + sum(rest...);
}

int main() {
    cout << sum(1, 2, 3, 4, 5) << "\n";              // 15
    cout << sum(1.5, 2.5, 3.5) << "\n";              // 7.5
    cout << sum(string("a"), string("b"), string("c")) << "\n";  // abc

    return 0;
}

Output:

15
7.5
abc

Unpacking Parameter Packs

// Print all arguments
template<typename... Args>
void printAll(Args... args) {
    // Use fold expression (C++17)
    (..., (cout << args << " "));  // Left fold
    cout << "\n";
}

int main() {
    printAll(1, 2.5, "hello", true);  // 1 2.5 hello 1

    return 0;
}

Template Metaprogramming: Compile-Time Computations

Templates are Turing-complete—they can do computations at compile time. This is powerful but advanced.

Factorial at Compile Time

// Compile-time factorial using template specialization
template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};

// Base case specialization
template<>
struct Factorial<0> {
    static constexpr int value = 1;
};

int main() {
    // All computed at compile time!
    cout << "5! = " << Factorial<5>::value << "\n";  // Computes: 120
    cout << "10! = " << Factorial<10>::value << "\n"; // Computes: 3628800

    // This is type-safe and zero runtime overhead
    constexpr int fiveFactorial = Factorial<5>::value;

    return 0;
}

Output:

5! = 120
10! = 3628800

Type Traits: Querying Type Information

// Check if a type is integral (compiler built-in)
#include <type_traits>

template<typename T>
void process(T value) {
    if constexpr (is_integral_v<T>) {
        cout << "Processing integer: " << value << "\n";
    } else if constexpr (is_floating_point_v<T>) {
        cout << "Processing float: " << value << "\n";
    } else {
        cout << "Processing other type\n";
    }
}

int main() {
    process(42);     // Processing integer: 42
    process(3.14);   // Processing float: 3.14
    process("text"); // Processing other type

    return 0;
}

Concepts (C++20): Constraining Templates Cleanly

Concepts let you specify requirements for template parameters. This makes templates more readable and errors clearer.

// Define a concept - what types must support
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> convertible_to<T>;  // Must support +
};

template<typename T>
concept Printable = requires(T x) {
    { cout << x };  // Must be printable
};

// Use concepts to constrain templates
template<Addable T>
T add(T a, T b) {
    return a + b;
}

template<Printable T>
void safePrint(T value) {
    cout << "Value: " << value << "\n";
}

int main() {
    cout << add(5, 10) << "\n";      // Works - int is Addable
    cout << add(3.5, 2.5) << "\n";   // Works - double is Addable

    safePrint(42);        // Works - int is Printable
    safePrint("hello");   // Works - const char* is Printable

    // Compile error if concept not satisfied
    // struct NonAddable {};
    // add(NonAddable(), NonAddable());  // ERROR!

    return 0;
}

Output:

15
6
Value: 42
Value: hello

Common Template Errors and How to Read Them

Template error messages are notoriously long. Here’s how to parse them:

template<typename T>
T getFirst(T arr[]) {
    return arr[0];
}

int main() {
    vector<int> vec = {1, 2, 3};

    // ERROR: vector doesn't decay to pointer
    // getFirst(vec);

    // To fix: write vector-aware template
    template<typename T>
    T getFirstVector(const vector<T>& vec) {
        return vec[0];
    }

    int arr[] = {1, 2, 3};
    cout << getFirst(arr) << "\n";  // Works - arrays decay to pointers

    return 0;
}

How to read error messages:

  1. Look for “template instantiation” - tells you which template was used
  2. Look for “no matching function” - template didn’t match your arguments
  3. Look at the constraints - does your type satisfy requirements?

typename vs class in Templates

Both typename and class work in template declarations:

// These are equivalent
template<typename T>
class Container1 {};

template<class T>
class Container2 {};

// Difference: typename is more explicit about intent
template<typename T>      // "T is a type"
class Container3 {};

// Use typename in type-dependent contexts
template<typename T>
void print(T value) {
    // When T is a class, this accesses a nested type
    typename T::iterator it;  // 'typename' tells compiler this is a type
}

Modern C++ prefers typename for clarity, but class is valid for backward compatibility.

Header-Only Implementation Requirement

Templates must be fully defined in headers. You cannot separate declaration and definition across files:

// stack.h
#ifndef STACK_H
#define STACK_H

#include <vector>
using namespace std;

template<typename T>
class Stack {
private:
    vector<T> elements;

public:
    // Definition MUST be in header
    void push(const T& value) {
        elements.push_back(value);
    }

    T pop() {
        T value = elements.back();
        elements.pop_back();
        return value;
    }
};

#endif

// This is wrong - don't do this:
// stack.cpp - DON'T PUT TEMPLATE DEFINITIONS HERE
// template<typename T>
// void Stack<T>::push(const T& value) { ... }
// Won't compile when you include stack.h in other files!

Why? When you use Stack<int>, the compiler must see the full template definition to instantiate it. If the definition is in a .cpp file, it’s not available when compiling your code.

Practical Example: Implementing a Generic Stack

Let’s build a complete, production-ready template:

#include <iostream>
#include <vector>
#include <stdexcept>
#include <type_traits>

using namespace std;

template<typename T>
class Stack {
private:
    vector<T> elements;

public:
    // Push element onto stack
    void push(const T& value) {
        elements.push_back(value);
    }

    // Pop and return top element
    T pop() {
        if (isEmpty()) {
            throw runtime_error("Stack underflow");
        }
        T value = elements.back();
        elements.pop_back();
        return value;
    }

    // Peek at top element without removing
    const T& peek() const {
        if (isEmpty()) {
            throw runtime_error("Stack is empty");
        }
        return elements.back();
    }

    // Check if stack is empty
    bool isEmpty() const {
        return elements.empty();
    }

    // Get number of elements
    size_t size() const {
        return elements.size();
    }

    // Clear stack
    void clear() {
        elements.clear();
    }

    // Template method for custom operations
    template<typename Func>
    void forEach(Func operation) const {
        for (const auto& element : elements) {
            operation(element);
        }
    }
};

int main() {
    // Integer stack
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    intStack.push(30);

    cout << "Integer stack (LIFO order):\n";
    while (!intStack.isEmpty()) {
        cout << intStack.pop() << " ";
    }
    cout << "\n\n";

    // String stack
    Stack<string> stringStack;
    stringStack.push("world");
    stringStack.push("hello");

    cout << "String stack:\n";
    cout << stringStack.pop() << " " << stringStack.pop() << "\n\n";

    // Using forEach with lambda
    Stack<int> nums;
    nums.push(1);
    nums.push(2);
    nums.push(3);

    cout << "Elements using forEach:\n";
    nums.forEach([](int x) { cout << x << " "; });
    cout << "\n\n";

    // Error handling
    try {
        Stack<int> emptyStack;
        emptyStack.pop();  // Throws
    } catch (const runtime_error& e) {
        cout << "Caught error: " << e.what() << "\n";
    }

    return 0;
}

Output:

Integer stack (LIFO order):
30 20 10

String stack:
hello world

Elements using forEach:
3 2 1

Caught error: Stack underflow

Advanced: Stack Specialization for Booleans

// Specialization for vector<bool> (which is optimized)
template<>
class Stack<bool> {
private:
    vector<bool> bits;

public:
    void push(bool value) {
        bits.push_back(value);
    }

    bool pop() {
        if (isEmpty()) throw runtime_error("Stack underflow");
        bool value = bits.back();
        bits.pop_back();
        return value;
    }

    bool isEmpty() const { return bits.empty(); }

    size_t size() const { return bits.size(); }

    void printBinary() const {
        for (bool bit : bits) {
            cout << (bit ? '1' : '0');
        }
        cout << "\n";
    }
};

int main() {
    Stack<bool> bitStack;
    bitStack.push(true);
    bitStack.push(false);
    bitStack.push(true);
    bitStack.push(true);

    cout << "Bits (bottom to top): ";
    bitStack.printBinary();  // 1011

    return 0;
}

Conclusion: Templates Are Powerful Tools

Templates enable you to write generic, type-safe code that the compiler specializes for your needs. Key takeaways:

  1. Function templates let you write functions once for many types
  2. Class templates create generic data structures
  3. Type deduction usually eliminates the need to specify template arguments
  4. Specialization lets you customize templates for specific types
  5. Header-only implementation is required—templates must be fully defined in headers
  6. Variadic templates handle variable numbers of arguments
  7. Concepts (C++20) make template requirements explicit
  8. Template metaprogramming enables compile-time computation

The learning curve is steep, but templates are worth mastering. They’re fundamental to modern C++ design and unlock capabilities impossible in other languages.

Next steps: Explore the Standard Template Library (STL). It’s built entirely on templates and demonstrates best practices. Try implementing other data structures like Queue, LinkedList, and HashMap as templates.

What template challenge are you facing? Templates take practice, but each problem you solve builds intuition.


Take Your C++ Further

If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.

👉 Get the C++ Better Explained Ebook — $19



Edit page
Share this post on:

Previous Post
Exception Handling in C++: Best Practices for Writing Robust, Error-Free Code
Next Post
C++ String Handling: std::string, string_view, and Performance Tips