Skip to content

Module 07: C++ Templates

Download Official Subject PDF

Key Concepts:

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

Templates enable generic programming—writing code once that works with any data type. The compiler generates specific versions as needed.

Templates power the entire Standard Template Library (STL)—every vector<T>, map<K,V>, and algorithm uses templates.


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

Implement three function templates:

  • swap(a, b) - exchange two values
  • min(a, b) - return the smaller value
  • max(a, b) - return the larger value

Key constraints:

  • Must work with any comparable type
  • When values are equal, return the second argument
  • Must work with the provided test main

Test main provided:

int main() {
int a = 2, 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", 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;
}

Think before coding:

  1. What is a template?

    • A blueprint for creating functions/classes
    • Compiler generates actual code when you use it
    • template <typename T> declares a type parameter
  2. Why use ::swap instead of swap?

    • :: means global namespace
    • Avoids conflict with std::swap
    • Your function must be in global scope
  3. Why return reference for min/max?

    • Efficiency (avoid copying)
    • Subject requirement for equal case
    • Returning reference to second argument when equal
  4. What operations must T support?

    • swap: assignment (=)
    • min: less-than comparison (<)
    • max: greater-than comparison (>)

Stage 1: Basic swap template

whatever.hpp
#ifndef WHATEVER_HPP
#define WHATEVER_HPP
template <typename T>
void swap(T& a, T& b) {
T temp = a; // T must support copy construction
a = b; // T must support assignment
b = temp;
}
#endif

Stage 2: Add min with correct equal behavior

whatever.hpp - min
template <typename T>
T const& min(T const& a, T const& b) {
// When equal, return b (second argument)
return (a < b) ? a : b;
}

Stage 3: Add max with correct equal behavior

whatever.hpp - max
template <typename T>
T const& max(T const& a, T const& b) {
// When equal, return b (second argument)
return (a > b) ? a : b;
}

Template syntax breakdown:

SyntaxMeaning
template <typename T>Declares T as a placeholder type
T& aReference to T (modifiable)
T const& aConst reference to T (read-only)
T const& min(...)Returns const reference to T

Why const references?

// By value - BAD: copies both arguments
T min(T a, T b) { return (a < b) ? a : b; }
// By const reference - GOOD: no copies
T const& min(T const& a, T const& b) { return (a < b) ? a : b; }

The equal case:

// When a == b, (a < b) is false, so return b
return (a < b) ? a : b; // Returns b when equal
// When a == b, (a > b) is false, so return b
return (a > b) ? a : b; // Returns b when equal

1. Return by value instead of reference:

// WRONG: Copies and loses "return second when equal" behavior
template <typename T>
T min(T a, T b) { return (a < b) ? a : b; }
// RIGHT: Returns reference to actual argument
template <typename T>
T const& min(T const& a, T const& b) { return (a < b) ? a : b; }

2. Wrong condition for equal case:

// WRONG: Returns first when equal
return (a <= b) ? a : b;
// RIGHT: Returns second when equal
return (a < b) ? a : b;

3. Not in global namespace:

// WRONG: In namespace, ::swap won't find it
namespace MyLib {
template <typename T>
void swap(T& a, T& b) { ... }
}
// RIGHT: In global scope
template <typename T>
void swap(T& a, T& b) { ... }

4. Using wrong parameter types for swap:

// WRONG: By value - swaps copies, originals unchanged
template <typename T>
void swap(T a, T b) {
T temp = a; a = b; b = temp;
}
// RIGHT: By reference - modifies original values
template <typename T>
void swap(T& a, T& b) {
T temp = a; a = b; b = temp;
}
main.cpp
#include <iostream>
#include <string>
#include "whatever.hpp"
int main() {
// Test with int
int a = 2, b = 3;
std::cout << "Before: a=" << a << ", b=" << b << std::endl;
::swap(a, b);
std::cout << "After swap: a=" << a << ", b=" << b << std::endl;
std::cout << "min: " << ::min(a, b) << std::endl;
std::cout << "max: " << ::max(a, b) << std::endl;
// Test with string
std::string c = "chaine1", d = "chaine2";
::swap(c, d);
std::cout << "c=" << c << ", d=" << d << std::endl;
std::cout << "min: " << ::min(c, d) << std::endl;
std::cout << "max: " << ::max(c, d) << std::endl;
// Test equal case - should return second
int x = 5, y = 5;
std::cout << "Equal test: &x=" << &x << ", &y=" << &y << std::endl;
std::cout << "min returns: " << &(::min(x, y)) << std::endl;
std::cout << "max returns: " << &(::max(x, y)) << std::endl;
// Should print &y for both!
return 0;
}

Expected output:

Before: a=2, b=3
After swap: a=3, b=2
min: 2
max: 3
c=chaine2, d=chaine1
min: chaine1
max: chaine2
Equal test: &x=0x7fff..., &y=0x7fff...
min returns: 0x7fff... (same as &y)
max returns: 0x7fff... (same as &y)
whatever.hpp
#ifndef WHATEVER_HPP
#define WHATEVER_HPP
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
template <typename T>
T const& min(T const& a, T const& b) {
return (a < b) ? a : b;
}
template <typename T>
T const& max(T const& a, T const& b) {
return (a > b) ? a : b;
}
#endif

Implement a function template iter that:

  • Takes an array address
  • Takes the array length
  • Takes a function to apply to each element
  • Calls the function on every element

Key insight: This is a generic algorithm - it works with any array type and any function.

Think before coding:

  1. What are the parameters?

    • T* array - pointer to first element
    • size_t length - number of elements
    • void (*func)(T&) - function pointer that takes element reference
  2. Do we need const version?

    • Yes! For read-only operations (like printing)
    • void (*func)(T const&) for const access
  3. How to call iter with a template function?

    • Must explicitly specify type: iter(arr, 5, print<int>)
    • Compiler can’t deduce template parameter from function pointer

Stage 1: Basic iter (modifying)

iter.hpp
#ifndef ITER_HPP
#define ITER_HPP
#include <cstddef> // size_t
template <typename T>
void iter(T* array, size_t length, void (*func)(T&)) {
for (size_t i = 0; i < length; i++) {
func(array[i]);
}
}
#endif

Stage 2: Add const version (reading)

iter.hpp - const overload
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]);
}
}

Stage 3: Test functions

main.cpp
#include <iostream>
#include "iter.hpp"
// Print function (const - doesn't modify)
template <typename T>
void print(T const& elem) {
std::cout << elem << " ";
}
// Double function (modifies)
template <typename T>
void doubleValue(T& elem) {
elem *= 2;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
std::cout << "Original: ";
iter(arr, 5, print<int>);
std::cout << std::endl;
iter(arr, 5, doubleValue<int>);
std::cout << "Doubled: ";
iter(arr, 5, print<int>);
std::cout << std::endl;
return 0;
}

Function pointer parameter:

SyntaxMeaning
void (*func)(T&)Pointer to function taking T& and returning void
void (*func)(T const&)Pointer to function taking T const& and returning void
func(array[i])Call the function with element as argument

Why two overloads?

// Modifying version - when function changes elements
void increment(int& n) { n++; }
iter(arr, 5, increment); // Uses void (*func)(T&)
// Const version - when function only reads
void print(int const& n) { std::cout << n; }
iter(arr, 5, print); // Uses void (*func)(T const&)

Template function as argument:

// Template function
template <typename T>
void print(T const& elem) { std::cout << elem; }
// WRONG: Can't deduce T
iter(arr, 5, print); // Error!
// RIGHT: Explicit type
iter(arr, 5, print<int>); // OK

1. Missing const overload:

// WRONG: Only non-const version
template <typename T>
void iter(T* array, size_t length, void (*func)(T&)) { ... }
// This fails:
void print(int const& n) { std::cout << n; }
iter(arr, 5, print); // Error! Can't convert function types
// RIGHT: Add const overload
template <typename T>
void iter(T* array, size_t length, void (*func)(T const&)) { ... }

2. Forgetting type specification:

template <typename T>
void print(T const& elem) { std::cout << elem; }
// WRONG: Compiler can't deduce
iter(arr, 5, print);
// RIGHT: Specify type
iter(arr, 5, print<int>);

3. Using wrong size type:

// WRONG: int for size (can be negative)
void iter(T* array, int length, ...) { ... }
// RIGHT: size_t (unsigned, correct type for sizes)
void iter(T* array, size_t length, ...) { ... }
main.cpp
#include <iostream>
#include <string>
#include "iter.hpp"
template <typename T>
void print(T const& elem) {
std::cout << "[" << elem << "] ";
}
template <typename T>
void increment(T& elem) {
elem++;
}
int main() {
// Test with int array
int numbers[] = {1, 2, 3, 4, 5};
std::cout << "Int array: ";
iter(numbers, 5, print<int>);
std::cout << std::endl;
iter(numbers, 5, increment<int>);
std::cout << "After increment: ";
iter(numbers, 5, print<int>);
std::cout << std::endl;
// Test with string array
std::string words[] = {"hello", "world", "test"};
std::cout << "String array: ";
iter(words, 3, print<std::string>);
std::cout << std::endl;
// Test with empty array
int empty[1] = {0};
iter(empty, 0, print<int>); // Should do nothing
std::cout << "Empty test passed" << std::endl;
return 0;
}

Test bash script:

Terminal window
c++ -Wall -Wextra -Werror -std=c++98 main.cpp -o iter
./iter
iter.hpp
#ifndef ITER_HPP
#define ITER_HPP
#include <cstddef>
template <typename T>
void iter(T* array, size_t length, void (*func)(T&)) {
for (size_t i = 0; i < length; i++) {
func(array[i]);
}
}
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]);
}
}
#endif
main.cpp
#include <iostream>
#include <string>
#include "iter.hpp"
template <typename T>
void print(T const& elem) {
std::cout << elem << " ";
}
template <typename T>
void doubleValue(T& elem) {
elem *= 2;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
std::cout << "Original: ";
iter(arr, 5, print<int>);
std::cout << std::endl;
iter(arr, 5, doubleValue<int>);
std::cout << "Doubled: ";
iter(arr, 5, print<int>);
std::cout << std::endl;
std::string strs[] = {"Hello", "World"};
std::cout << "Strings: ";
iter(strs, 2, print<std::string>);
std::cout << std::endl;
return 0;
}

Implement a class template Array<T> that:

  • Stores elements of any type T
  • Supports default construction (empty array)
  • Supports size construction with elements initialized by default
  • Full OCF (copy constructor, assignment, destructor)
  • operator[] with bounds checking (throws exception)
  • size() member function

Key constraints:

  • Memory allocated with new[]
  • Must use exceptions for out-of-bounds
  • Deep copy required (changes to copy don’t affect original)

Think before coding:

  1. What members do we need?

    • T* _array - dynamically allocated array
    • unsigned int _size - number of elements
  2. Why deep copy?

    • Each Array owns its own memory
    • Shallow copy = two Arrays pointing to same memory = disaster
  3. How to initialize elements by default?

    • new T[n]() - the () triggers value initialization
    • Integers become 0, pointers become NULL, objects use default constructor
  4. Implementation in header only - why?

    • Templates require implementation visible at instantiation
    • Compiler generates code when you use Array<int>, Array<std::string>, etc.
    • Can’t be in .cpp file (linker won’t find it)

Stage 1: Class skeleton with members

Array.hpp
#ifndef ARRAY_HPP
#define ARRAY_HPP
#include <stdexcept> // std::out_of_range
template <typename T>
class Array {
private:
T* _array;
unsigned int _size;
public:
// Constructors & Destructor
Array();
Array(unsigned int n);
Array(const Array& other);
~Array();
// Assignment
Array& operator=(const Array& other);
// Element access
T& operator[](unsigned int index);
const T& operator[](unsigned int index) const;
// Size getter
unsigned int size() const;
};
#endif

Stage 2: Default and size constructors

Array.hpp - constructors
template <typename T>
Array<T>::Array() : _array(NULL), _size(0) {}
template <typename T>
Array<T>::Array(unsigned int n) : _array(new T[n]()), _size(n) {}

Stage 3: Copy constructor and destructor

Array.hpp - copy & destructor
template <typename T>
Array<T>::Array(const Array& other) : _array(NULL), _size(0) {
*this = other; // Use assignment operator
}
template <typename T>
Array<T>::~Array() {
delete[] _array;
}

Stage 4: Assignment operator (deep copy)

Array.hpp - assignment
template <typename T>
Array<T>& Array<T>::operator=(const Array& other) {
if (this != &other) {
delete[] _array; // Free old memory
_size = other._size;
_array = new T[_size];
for (unsigned int i = 0; i < _size; i++) {
_array[i] = other._array[i];
}
}
return *this;
}

Stage 5: Element access with bounds checking

Array.hpp - operator[]
template <typename T>
T& Array<T>::operator[](unsigned int index) {
if (index >= _size) {
throw std::out_of_range("Array index out of bounds");
}
return _array[index];
}
template <typename T>
const T& Array<T>::operator[](unsigned int index) const {
if (index >= _size) {
throw std::out_of_range("Array index out of bounds");
}
return _array[index];
}
template <typename T>
unsigned int Array<T>::size() const {
return _size;
}

Value initialization:

SyntaxEffect
new T[n]Default initialization (garbage for primitives)
new T[n]()Value initialization (0 for primitives)
// Without () - contains garbage
int* arr1 = new int[5]; // arr1[0] = ??? (undefined)
// With () - initialized to zero
int* arr2 = new int[5](); // arr2[0] = 0

Deep copy in assignment:

template <typename T>
Array<T>& Array<T>::operator=(const Array& other) {
if (this != &other) { // Self-assignment check
delete[] _array; // Free existing memory
_size = other._size; // Copy size
_array = new T[_size]; // Allocate new memory
for (unsigned int i = 0; i < _size; i++) {
_array[i] = other._array[i]; // Copy each element
}
}
return *this;
}

Why two operator[] overloads?

// Non-const: for modifying elements
T& operator[](unsigned int index);
arr[0] = 42; // Uses non-const
// Const: for reading from const Array
const T& operator[](unsigned int index) const;
const Array<int> arr(5);
int x = arr[0]; // Uses const version

1. Shallow copy:

// WRONG: Both arrays point to same memory
Array(const Array& other) {
_size = other._size;
_array = other._array; // Shallow copy!
}
// When one is destroyed, other has dangling pointer
// RIGHT: Allocate new memory and copy elements
Array(const Array& other) : _array(NULL), _size(0) {
*this = other; // Uses deep-copy assignment
}

2. Missing value initialization:

// WRONG: Garbage values for primitives
Array(unsigned int n) : _array(new T[n]), _size(n) {}
// RIGHT: Value-initialized (zeros for int, etc.)
Array(unsigned int n) : _array(new T[n]()), _size(n) {}

3. Not checking self-assignment:

// WRONG: Deletes own data before copying
Array& operator=(const Array& other) {
delete[] _array; // Oops, deleted our own data!
_array = new T[other._size];
// ... copy from other (which is now garbage)
}
// RIGHT: Check first
Array& operator=(const Array& other) {
if (this != &other) {
delete[] _array;
// ... safe to copy
}
return *this;
}

4. Using int instead of unsigned int:

// WRONG: Signed comparison can be problematic
if (index >= _size) // Warning: comparing signed and unsigned
// Subject specifies unsigned int
T& operator[](unsigned int index);

5. Implementation in .cpp file:

Array.cpp
// WRONG: Linker error
template <typename T>
Array<T>::Array() { ... }
// RIGHT: Implementation must be in header
// Array.hpp (or Array.tpp included by .hpp)
template <typename T>
Array<T>::Array() { ... }
main.cpp
#include <iostream>
#include "Array.hpp"
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 5 array: " << arr.size() << std::endl;
// Test value initialization
std::cout << "Initial values: ";
for (unsigned int i = 0; i < arr.size(); i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// Test modification
for (unsigned int i = 0; i < arr.size(); i++) {
arr[i] = i * 10;
}
std::cout << "After modification: ";
for (unsigned int i = 0; i < arr.size(); i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// Test deep copy
Array<int> copy = arr;
copy[0] = 999;
std::cout << "Original[0]: " << arr[0] << std::endl;
std::cout << "Copy[0]: " << copy[0] << std::endl;
// Test bounds checking
try {
arr[100] = 42;
} catch (std::exception& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
// Test with different type
Array<std::string> strings(3);
strings[0] = "Hello";
strings[1] = "World";
strings[2] = "!";
for (unsigned int i = 0; i < strings.size(); i++) {
std::cout << strings[i] << " ";
}
std::cout << std::endl;
return 0;
}

Test bash script:

Terminal window
c++ -Wall -Wextra -Werror -std=c++98 main.cpp -o array_test
./array_test
# Expected output:
# Empty size: 0
# Size 5 array: 5
# Initial values: 0 0 0 0 0
# After modification: 0 10 20 30 40
# Original[0]: 0
# Copy[0]: 999
# Exception: Array index out of bounds
# Hello World !
Array.hpp
#ifndef ARRAY_HPP
#define ARRAY_HPP
#include <stdexcept>
template <typename T>
class Array {
private:
T* _array;
unsigned int _size;
public:
Array();
Array(unsigned int n);
Array(const Array& other);
~Array();
Array& operator=(const Array& other);
T& operator[](unsigned int index);
const T& operator[](unsigned int index) const;
unsigned int size() const;
};
// Default constructor
template <typename T>
Array<T>::Array() : _array(NULL), _size(0) {}
// Size constructor with value initialization
template <typename T>
Array<T>::Array(unsigned int n) : _array(new T[n]()), _size(n) {}
// Copy constructor
template <typename T>
Array<T>::Array(const Array& other) : _array(NULL), _size(0) {
*this = other;
}
// Destructor
template <typename T>
Array<T>::~Array() {
delete[] _array;
}
// Assignment operator (deep copy)
template <typename T>
Array<T>& Array<T>::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;
}
// Element access (non-const)
template <typename T>
T& Array<T>::operator[](unsigned int index) {
if (index >= _size) {
throw std::out_of_range("Array index out of bounds");
}
return _array[index];
}
// Element access (const)
template <typename T>
const T& Array<T>::operator[](unsigned int index) const {
if (index >= _size) {
throw std::out_of_range("Array index out of bounds");
}
return _array[index];
}
// Size getter
template <typename T>
unsigned int Array<T>::size() const {
return _size;
}
#endif
main.cpp
#include <iostream>
#include <string>
#include "Array.hpp"
int main() {
// Test empty array
Array<int> empty;
std::cout << "Empty size: " << empty.size() << std::endl;
// Test size constructor
Array<int> arr(5);
std::cout << "Values initialized to: ";
for (unsigned int i = 0; i < arr.size(); i++)
std::cout << arr[i] << " ";
std::cout << std::endl;
// Test modification
for (unsigned int i = 0; i < arr.size(); i++)
arr[i] = i * 10;
// Test deep copy
Array<int> copy(arr);
copy[0] = 999;
std::cout << "Original[0]=" << arr[0] << ", Copy[0]=" << copy[0] << std::endl;
// Test exception
try {
std::cout << arr[100] << std::endl;
} catch (std::exception& e) {
std::cout << "Exception caught: " << e.what() << std::endl;
}
return 0;
}

ConceptKey Point
Function templatetemplate <typename T> before function
Class templatetemplate <typename T> before class
InstantiationCompiler generates code for each type used
Header-onlyTemplates must be defined where used
Type requirementsT must support operations used on it

Template syntax reference:

// Function template
template <typename T>
T const& min(T const& a, T const& b);
// Class template
template <typename T>
class Array {
T* data;
public:
Array();
T& operator[](unsigned int i);
};
// Method definition outside class
template <typename T>
Array<T>::Array() : data(NULL) {}
// Using templates
Array<int> intArray(10);
Array<std::string> strArray(5);

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

Continue your C++ journey:

Visit the Glossary for definitions of: