Skip to content

Module 07: C++ Templates

Download Official Subject PDF

Key Concepts:

  • Function templates
  • Class templates
  • Template instantiation
  • Template specialization

Before C++ templates, C programmers used parametric macros to write generic code:

#define MAX(x, y) ((x) > (y) ? (x) : (y))
int result = MAX(3, 5); // Works
double d = MAX(3.14, 2.71); // Works

But macros have serious problems:

  1. No type safety: MAX("hello", 42) compiles but is nonsense
  2. Text substitution bugs: MAX(i++, j) increments i twice if i > j
  3. No scope: Macros pollute the global namespace
  4. No debugging: Errors point to expanded code, not the macro

Templates solve all these problems. The compiler:

  • Checks types at compile time
  • Generates real functions (no text substitution)
  • Respects scope rules
  • Provides (albeit verbose) type-checked error messages

Think of templates as code patterns with holes (placeholders) that the compiler fills in:

template <typename T> // "T" is the hole
T max(T a, T b) { // Pattern with holes
return (a > b) ? a : b;
}
max(3, 5); // Compiler fills hole with "int"
max(3.14, 2.71); // Compiler fills hole with "double"

When you use a template, the compiler instantiates it: it takes your pattern, fills in the holes with actual types, and generates real code. This is compile-time code generation, not runtime polymorphism.


This module introduces templates. Here’s the new syntax explained:

template <typename T>
T max(T a, T b) { return (a > b) ? a : b; }
  • template <> declares that what follows is a template
  • typename T (or class T) declares a type parameter called T
  • T can be any type: int, double, std::string, your own class, etc.
  • When you use the template, the compiler generates code for the specific type
  • typename and class are interchangeable in C++98 (both mean the same thing)

The <> Angle Brackets (Template Parameters)

Section titled “The <> Angle Brackets (Template Parameters)”
max<int>(3, 5); // Explicitly use int
max(3.14, 2.71); // Compiler deduces double
Array<int> intArray; // Class template instantiation
  • <> enclose template arguments
  • For functions, you can let the compiler deduce the type from arguments
  • For classes, you must explicitly specify the type
  • Read Array<int> as “Array of int”
std::vector<int>::iterator it;
  • :: with templates accesses nested types
  • vector<int>::iterator means “the iterator type inside vector-of-int”
  • Template classes can have their own nested types (types that depend on the template parameter)
typename std::vector<T>::iterator it; // Tell compiler this is a type
  • typename before ClassName::NestedType tells the compiler “this is a type, not a value”
  • Inside templates, the compiler doesn’t know if vector<T>::iterator is a type or static member
  • typename removes this ambiguity - it says “trust me, this is a type name”
  • Required when accessing nested types of template-dependent classes
template <typename T, typename U>
T convert(U value) { return static_cast<T>(value); }
  • You can have multiple template parameters separated by commas
  • T is the return type, U is the input type
  • Usage: convert<int>(3.14) explicitly specifies T, compiler deduces U
template <typename T = int>
class Stack { };
Stack<> intStack; // Uses default: int
Stack<double> dblStack; // Explicit: double
  • = int provides a default type parameter
  • If not specified, the default is used
  • Use <> when you want the default (not just Stack)

// Without templates - must write for each type
int maxInt(int a, int b) { return (a > b) ? a : b; }
double maxDouble(double a, double b) { return (a > b) ? a : b; }
std::string maxString(std::string a, std::string b) { return (a > b) ? a : b; }
// With templates - one definition, works for all types
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
// Usage
max(3, 5); // Instantiates max<int>
max(3.14, 2.71); // Instantiates max<double>
max(str1, str2); // Instantiates max<std::string>

template <typename T>
T functionName(T param1, T param2) {
// T can be used like any type
return param1 + param2;
}
template <typename T, typename U>
T convert(U value) {
return static_cast<T>(value);
}
// Usage
int i = convert<int>(3.14); // Explicit T, deduced U
template <typename T> // Modern style
template <class T> // Also valid, means the same thing

// swap: Exchange values of two variables
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
// min: Return smaller value (return second if equal)
template <typename T>
T const& min(T const& a, T const& b) {
return (a < b) ? a : b;
}
// max: Return larger value (return second if equal)
template <typename T>
T const& max(T const& a, T const& b) {
return (a > b) ? a : b;
}
int main() {
int a = 2;
int b = 3;
::swap(a, b);
std::cout << "a = " << a << ", b = " << b << std::endl;
std::cout << "min(a, b) = " << ::min(a, b) << std::endl;
std::cout << "max(a, b) = " << ::max(a, b) << std::endl;
std::string c = "chaine1";
std::string d = "chaine2";
::swap(c, d);
std::cout << "c = " << c << ", d = " << d << std::endl;
std::cout << "min(c, d) = " << ::min(c, d) << std::endl;
std::cout << "max(c, d) = " << ::max(c, d) << std::endl;
return 0;
}

// iter: Apply function to each element of array
// F can be a function pointer OR a functor type
template <typename T, typename F>
void iter(T* array, size_t length, F func) {
for (size_t i = 0; i < length; i++) {
func(array[i]);
}
}
// More explicit: F is specifically a function pointer
template <typename T>
void iter(T* array, size_t length, void (*func)(T&)) {
for (size_t i = 0; i < length; i++) {
func(array[i]);
}
}

The same template can be overloaded for const and non-const operations:

// Non-const version - can modify elements
template <typename T>
void iter(T* array, size_t length, void (*func)(T&)) {
for (size_t i = 0; i < length; i++) {
func(array[i]);
}
}
// Const version - read-only operations
template <typename T>
void iter(T* array, size_t length, void (*func)(T const&)) {
for (size_t i = 0; i < length; i++) {
func(array[i]);
}
}
// Function that modifies (needs non-const reference)
template <typename T>
void increment(T& elem) {
elem++;
}
// Function that only reads (uses const reference)
template <typename T>
void print(T const& elem) {
std::cout << elem << std::endl;
}
int arr[] = {1, 2, 3};
// Calls non-const version
iter(arr, 3, increment<int>); // arr is now {2, 3, 4}
// Calls const version
iter(arr, 3, print<int>); // Prints elements
// When calling with a template function, you must instantiate it:
iter(arr, 3, print<int>); // Explicit template argument
iter(arr, 3, &print<int>); // & is optional
// With non-template functions:
void printInt(int const& x) { std::cout << x; }
iter(arr, 3, printInt); // No <> needed
template <typename T>
void print(T const& elem) {
std::cout << elem << std::endl;
}
template <typename T>
void increment(T& elem) {
elem++;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
std::cout << "Original:" << std::endl;
iter(arr, 5, print<int>);
iter(arr, 5, increment<int>);
std::cout << "After increment:" << std::endl;
iter(arr, 5, print<int>);
return 0;
}

template <typename T>
class Container {
private:
T* _data;
size_t _size;
public:
Container(size_t size);
Container(const Container& other);
Container& operator=(const Container& other);
~Container();
T& operator[](size_t index);
const T& operator[](size_t index) const;
size_t size() const;
};
// For class templates, implementation MUST be in header
// Or in a .tpp file included by the header
template <typename T>
Container<T>::Container(size_t size) : _size(size) {
_data = new T[size](); // () for value initialization
}
template <typename T>
Container<T>::~Container() {
delete[] _data;
}
template <typename T>
T& Container<T>::operator[](size_t index) {
if (index >= _size)
throw std::out_of_range("Index out of bounds");
return _data[index];
}

template <typename T>
class Array {
private:
T* _array;
unsigned int _size;
public:
// Default constructor - empty array
Array() : _array(NULL), _size(0) {}
// Size constructor - array of n elements
Array(unsigned int n) : _array(new T[n]()), _size(n) {}
// Copy constructor - deep copy
Array(const Array& other) : _array(NULL), _size(0) {
*this = other;
}
// Assignment operator - deep copy
Array& operator=(const Array& other) {
if (this != &other) {
delete[] _array;
_size = other._size;
_array = new T[_size];
for (unsigned int i = 0; i < _size; i++)
_array[i] = other._array[i];
}
return *this;
}
// Destructor
~Array() {
delete[] _array;
}
// Element access with bounds checking
T& operator[](unsigned int index) {
if (index >= _size)
throw std::out_of_range("Index out of bounds");
return _array[index];
}
const T& operator[](unsigned int index) const {
if (index >= _size)
throw std::out_of_range("Index out of bounds");
return _array[index];
}
// Size getter
unsigned int size() const {
return _size;
}
};
#include <stdexcept> // for std::out_of_range
// out_of_range is used for index/bounds errors
T& operator[](unsigned int index) {
if (index >= _size)
throw std::out_of_range("Index out of bounds");
return _array[index];
}
// Usage:
try {
Array<int> arr(5);
arr[10] = 42; // Throws out_of_range
}
catch (std::out_of_range& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
catch (std::exception& e) {
// Catches any standard exception
std::cout << "Error: " << e.what() << std::endl;
}
std::exception
├── std::logic_error
│ ├── std::out_of_range <- Use for index errors
│ ├── std::invalid_argument <- Use for bad function arguments
│ └── std::length_error <- Use for size limit errors
└── std::runtime_error <- Use for general runtime errors
int main() {
// Test default constructor
Array<int> empty;
std::cout << "Empty size: " << empty.size() << std::endl;
// Test size constructor
Array<int> arr(5);
std::cout << "Size: " << arr.size() << std::endl;
// Test element access
for (unsigned int i = 0; i < arr.size(); i++)
arr[i] = i * 2;
// Test copy
Array<int> copy(arr);
arr[0] = 100;
std::cout << "arr[0]: " << arr[0] << std::endl;
std::cout << "copy[0]: " << copy[0] << std::endl; // Should be 0
// Test bounds checking
try {
arr[100] = 42;
}
catch (std::exception& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
return 0;
}

// Compiler generates code when template is used
Array<int> intArray; // Generates Array<int>
Array<double> dblArray; // Generates Array<double>
// Force generation of specific types (rarely needed)
template class Array<int>;
template void swap<double>(double&, double&);

Array.hpp
template <typename T>
class Array {
// Declaration AND implementation here
};
Array.hpp
template <typename T>
class Array {
// Declaration only
};
#include "Array.tpp" // Include implementation at end
// Array.tpp
template <typename T>
Array<T>::Array() { /* ... */ }

The .tpp naming convention: Using .tpp (template implementation) instead of .cpp clearly identifies template implementation files in your codebase. When you see a .tpp file, you immediately know it contains template definitions that will be included in a header.

Templates need to be visible at instantiation point. If implementation is in .cpp, the compiler can’t generate the code.


Sometimes you need different behavior for specific types. Template specialization lets you provide custom implementations for particular template arguments.

Complete Specialization (Functions and Classes)

Section titled “Complete Specialization (Functions and Classes)”
// Generic template
template <typename T>
void print(T value) {
std::cout << value << std::endl;
}
// Complete specialization for bool
template <>
void print<bool>(bool value) {
std::cout << (value ? "true" : "false") << std::endl;
}
// Usage
print(42); // Uses generic: prints "42"
print(3.14); // Uses generic: prints "3.14"
print(true); // Uses specialization: prints "true"
// Generic template
template <typename T>
class Storage {
T _data;
public:
void store(T val) { _data = val; }
T retrieve() { return _data; }
};
// Complete specialization for char*
template <>
class Storage<char*> {
std::string _data; // Store as string for safety
public:
void store(char* val) { _data = val; }
const char* retrieve() { return _data.c_str(); }
};

Partial specialization provides a template for a subset of types (e.g., all pointers):

// Generic template
template <typename T>
class Container {
public:
void info() { std::cout << "Generic container" << std::endl; }
};
// Partial specialization for all pointer types
template <typename T>
class Container<T*> {
public:
void info() { std::cout << "Pointer container" << std::endl; }
};
// Usage
Container<int> c1; // Uses generic
Container<int*> c2; // Uses pointer specialization
Container<double*> c3; // Uses pointer specialization

Note: Function templates cannot be partially specialized. If you need partial specialization behavior for functions, use overloading or a helper class template.

  • Optimization: Provide faster implementations for specific types
  • Special behavior: Handle types that don’t work with the generic implementation
  • Type traits: Build compile-time type information (like is_pointer above)

Template error messages are notoriously difficult to read. A small mistake can generate pages of errors. This is a known challenge with templates, not a personal failure.

  1. Read from the bottom up: The actual error is often near the end; earlier messages show instantiation context

  2. Look for “instantiated from”: This tells you WHERE the template was used, helping you trace the problem

  3. Test with concrete types first: If your template doesn’t work, try writing the code for a specific type (like int) first

  4. Check required operations: If your template uses <, the type must support operator<

// This template requires operator< to exist for type T
template <typename T>
T min(T a, T b) {
return (a < b) ? a : b;
}
// This will fail with a confusing error:
struct NoCompare { int x; };
NoCompare a, b;
min(a, b); // Error: no operator< for NoCompare
  1. Simplify: Comment out parts of your template to isolate the problem

template <typename T>
struct is_pointer {
static const bool value = false;
};
template <typename T>
struct is_pointer<T*> {
static const bool value = true;
};
template <typename T = int>
class Stack {
// Default to int if no type specified
};
Stack<> intStack; // Uses default: int
Stack<double> dblStack; // Explicit: double

template <typename T>
T functionName(T param) { /* ... */ }
template <typename T>
class ClassName {
// Members using T
};
// Outside class definition
template <typename T>
ClassName<T>::methodName() { /* ... */ }
  1. Templates must be in headers (or included .tpp)
  2. Use typename or class (interchangeable)
  3. Types must support operations used in template
  4. Deep copy for pointer members in class templates