Skip to content

Introduction to Templates

Video: Back to Basics: Function and Class Templates - Dan Saks - CppCon 2019

C++ templates are a powerful feature that enables generic programming - writing code that works with multiple types without sacrificing type safety or performance. Templates are the foundation of the Standard Template Library (STL) and modern C++ programming.

What are Templates?

Templates are a way to write generic code that can work with different data types. They allow you to write a function or class once and use it with any type that supports the required operations.

Key concept: Templates are a compile-time mechanism which allows the compiler to generate specific versions of your code for each type you use.

Function Templates

Function templates allow you to write generic functions that work with multiple types:

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

int main() {
    int max_int = max(5, 3);           // T = int
    double max_double = max(3.14, 2.71); // T = double
    std::string max_str = max("hello", "world"); // T = std::string

    std::cout << max_int << std::endl;     // 5
    std::cout << max_double << std::endl;  // 3.14
    std::cout << max_str << std::endl;     // "world"
}

Template instantiation: When you call max(5, 3), the compiler generates a specific version of the function for int:

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

Class Templates

Class templates allow you to create generic classes:

cpp
template<typename T>
class Stack {
private:
    T* data;
    int size;
    int capacity;

public:
    Stack(int cap = 10) : size(0), capacity(cap) {
        data = new T[capacity];
    }

    ~Stack() {
        deletedata;
    }

    void push(const T& item) {
        if (size < capacity) {
            data[size++] = item;
        }
    }

    T pop() {
        if (size > 0) {
            return data[--size];
        }
        throw std::runtime_error("Stack empty");
    }

    T top() const {
        if (size > 0) {
            return data[size - 1];
        }
        throw std::runtime_error("Stack empty");
    }

    int getSize() const { return size; }
};

int main() {
    Stack<int> int_stack;      // Stack of integers
    Stack<std::string> str_stack; // Stack of strings

    int_stack.push(42);
    str_stack.push("hello");
}

Template Parameters

Type Parameters (typename/class)

cpp
template<typename T>  // or template<class T>
class Container {
    T data;
public:
    Container(const T& value) : data(value) {}
    T get() const { return data; }
};

// Multiple type parameters
template<typename T1, typename T2>
class Pair {
    T1 first;
    T2 second;
public:
    Pair(const T1& f, const T2& s) : first(f), second(s) {}
};

Non-Type Parameters

cpp
template<typename T, int SIZE>
class Array {
    T data[SIZE];
public:
    T& operator(int index) {
        return data[index];
    }

    int size() const { return SIZE; }
};

int main() {
    Array<int, 10> int_array;    // Array of 10 integers
    Array<double, 5> double_array; // Array of 5 doubles
}

How Templates Work: Deep Implementation

Compile-Time Code Generation

Templates work through a process called template instantiation:

  1. Template Definition: You write the template code
  2. Template Instantiation: When you use the template with specific types, the compiler generates concrete code
  3. Code Generation: The compiler creates a separate version of the function/class for each type combination
cpp
template<typename T>
T add(T a, T b) { return a + b; }

int main() {
    add(5, 3);      // Compiler generates: int add(int a, int b) { return a + b; }
    add(3.14, 2.71); // Compiler generates: double add(double a, double b) { return a + b; }
}

Result: The executable contains two different add functions, one for int and one for double.

Template vs Java Generics

AspectC++ TemplatesJava Generics
ImplementationCompile-time code generationRuntime type erasure
PerformanceZero overhead - no runtime costRuntime overhead due to boxing/unboxing
Type SafetyCompile-time type checkingRuntime type checking with erasure
Code GenerationSeparate code for each typeSingle code with type erasure
SpecializationFull specialization supportLimited (only inheritance)
Non-type paramsSupported (int, pointers, etc.)Not supported

Java Example:

cpp
// Java generics - type erasure at runtime
public class Box<T> {
    private T data;
    public void set(T data) { this.data = data; }
    public T get() { return data; }
}

// At runtime, this becomes:
public class Box {
    private Object data;  // Type information is lost
    public void set(Object data) { this.data = data; }
    public Object get() { return data; }
}

C++ Example:

cpp
// C++ templates - separate code generation
template<typename T>
class Box {
    T data;
public:
    void set(const T& data) { this->data = data; }
    T get() const { return data; }
};

// Compiler generates separate classes:
class Box_int { int data; /* ... */ };
class Box_double { double data; /* ... */ };
class Box_string { std::string data; /* ... */ };

Template vs C Macros

AspectC++ TemplatesC Macros
Type SafetyFull compile-time type checkingNo type checking
DebuggingFull debugger supportNo debugger support
ScopeRespect C++ scoping rulesGlobal text replacement
PerformanceOptimized by compilerDirect text substitution
ComplexityCan be complex but manageableLimited and error-prone

C Macro Example:

cpp
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    int result = MAX(5, 3);  // Works
    double d_result = MAX(3.14, 2.71);  // Works but no type safety
    char* str_result = MAX("hello", "world");  // Compiles but wrong!
}

C++ Template Example:

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

int main() {
    int result = max(5, 3);           // Works
    double d_result = max(3.14, 2.71); // Works with type safety
    // max("hello", "world");         // Compile error - no operator> for char*
}

Best Practices

  1. Use typename for type parameters: template<typename T> is preferred over template<class T>
  2. Provide constraints: Use concepts (C++20) or SFINAE to constrain template parameters
  3. Avoid template bloat: Be mindful of code size increase
  4. Use specialization carefully: Only specialize when necessary
  5. Document requirements: Clearly state what operations the template type must support