Skip to content

Module 07: C++ Templates

Download Official Subject PDF

Key Concepts:

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

// 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() { /* ... */ }

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


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