Skip to content

Module 07 Tutorial

Prerequisites: Review Module 07 Concepts first.

This module introduces C++ templates - the foundation of generic programming. You’ll learn to write code that works with any type through function templates and class templates.


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);