Skip to content

Module 02: Operator Overloading and Orthodox Canonical Form

Download Official Subject PDF

Key Concepts:

  • Orthodox Canonical Form (OCF)
  • Copy constructor
  • Copy assignment operator
  • Operator overloading
  • Fixed-point numbers

This module introduces two critical concepts: OCF (Orthodox Canonical Form) and operator overloading.

Without proper copy semantics, your classes will have bugs. If your class manages dynamic memory and you don’t define a copy constructor, C++ will create a “shallow copy” that shares pointers—leading to double-delete crashes.

Operator overloading lets your custom types work naturally with operators. Instead of point1.add(point2), you can write point1 + point2. This makes code more readable and intuitive.


In C, if you wanted functions to work with different types, you had to give them different names:

// C approach - different names for different types
int max_int(int a, int b);
double max_double(double a, double b);
float max_float(float a, float b);

C++ introduced function overloading: functions with the same name but different parameter types. The compiler chooses the right one based on the arguments:

// C++ approach - same name, compiler picks the right one
int max(int a, int b);
double max(double a, double b);
float max(float a, float b);
max(3, 5); // Calls max(int, int)
max(3.14, 2.71); // Calls max(double, double)

This is a form of ad-hoc polymorphism: the same function name adapts to different types. Operator overloading extends this idea to operators like +, -, <<, etc.


This module teaches you how to define custom behavior for operators. Here’s what the new syntax means:

Fixed a;
Fixed b;
a = b; // Calls the assignment operator
  • = assigns one object’s value to another EXISTING object
  • You can customize it by defining operator= in your class
  • The compiler provides a default, but for classes with pointers you MUST write your own
  • Unlike the copy constructor (which creates a NEW object), assignment works on EXISTING objects
if (this != &other) { // Check for self-assignment
  • &other gets the memory address of the other object
  • this is a pointer to the current object
  • this != &other compares two addresses to check if they’re the same object
  • This prevents bugs when someone writes a = a;
return *this; // Return the current object (not the pointer!)
  • *this dereferences the this pointer to get the actual object
  • Assignment operators return *this so you can chain assignments: a = b = c;
  • Without the *, you’d return a pointer, which wouldn’t allow chaining
// As member function:
bool operator==(const Fixed& other) const {
return _rawValue == other._rawValue;
}
// As non-member function (must be friend or use public interface):
std::ostream& operator<<(std::ostream& os, const Fixed& fixed) {
os << fixed.toFloat();
return os;
}
  • operator@ (where @ is any operator) defines custom behavior for that operator
  • Member operators get one implicit parameter: this (the left operand)
  • Non-member operators need both operands as explicit parameters
  • << and >> are usually non-member because the left side is std::ostream/std::istream (not your class)
// Pre-increment: ++a
Fixed& operator++() {
_rawValue++; // Increment first
return *this; // Return the incremented object
}
// Post-increment: a++
Fixed operator++(int) { // The (int) is just a marker!
Fixed temp(*this); // Save current value
_rawValue++; // Increment
return temp; // Return the OLD value
}
  • ++ and -- can be prefix (++a) or postfix (a++)
  • The (int) parameter distinguishes postfix from prefix (it’s not used, just a marker)
  • Pre-increment returns by reference (faster, returns the object itself)
  • Post-increment returns by value (slower, returns a copy of the old value)

The << Operator (Bit Shift - Fixed Point Math)

Section titled “The << Operator (Bit Shift - Fixed Point Math)”
_rawValue = (_rawValue << 8); // Shift left by 8 bits
_rawValue = (_rawValue >> 8); // Shift right by 8 bits
  • << and >> also perform bitwise shifts (unrelated to stream operators!)
  • << 8 multiplies by 2^8 (256) - shifts bits to the left
  • >> 8 divides by 2^8 (256) - shifts bits to the right
  • Used in fixed-point arithmetic for fractional calculations
  • The same symbols do completely different things depending on context!
Fixed(const Fixed& other); // Copy constructor
  • & after the type means “reference to”
  • const means “don’t modify the original”
  • const Fixed& is a “const reference” - efficient (no copy) and safe (read-only)
  • The copy constructor takes a const reference to the source object
  • Without &, you’d create infinite recursion (copy constructor calling itself!)

Design Philosophy: OCF is not just a rule - it’s a design pattern that ensures all your classes have a consistent, predictable interface. When every class follows OCF:

  • Other programmers know what to expect from any class
  • Copy/assignment behavior is explicit and documented
  • Resource management is properly handled
  • Your code integrates smoothly with STL containers and algorithms

Think of OCF as establishing a “contract” that every class fulfills.

From Module 02 onwards, ALL classes must implement:

class Sample {
public:
Sample(); // 1. Default constructor
Sample(const Sample& other); // 2. Copy constructor
Sample& operator=(const Sample& other); // 3. Copy assignment operator
~Sample(); // 4. Destructor
};
┌─────────────────────────────────────────────────────────────────┐
│ Orthodox Canonical Form │
│ (The Rule of Four) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ Default │───→ Creates object with default values │
│ │ Constructor │ MyClass a; │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Copy │───→ Creates NEW object from existing │
│ │ Constructor │ MyClass b(a); // or MyClass b = a; │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Copy Assignment │───→ Assigns to EXISTING object │
│ │ Operator │ a = b; // both already exist │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Destructor │───→ Cleans up when object is destroyed │
│ │ │ (automatically called) │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Why all four matter: If your class manages dynamic memory (pointers), the compiler-generated defaults will cause double-delete bugs. You must implement all four to properly manage resource lifecycle.

Without proper OCF, classes with dynamic memory will have bugs:

// BAD: No copy constructor or assignment operator
class Bad {
private:
int* _data;
public:
Bad() { _data = new int(42); }
~Bad() { delete _data; }
};
Bad a;
Bad b = a; // Default copy: b._data = a._data (same pointer!)
// When a and b are destroyed: double delete! CRASH!

Creates a NEW object as a copy of an existing object.

class Sample {
private:
int _value;
int* _data;
public:
// Copy constructor
Sample(const Sample& other) : _value(other._value) {
std::cout << "Copy constructor called" << std::endl;
// Deep copy: allocate new memory and copy content
_data = new int(*other._data);
}
};
Sample a;
Sample b(a); // Copy constructor
Sample c = a; // Copy constructor (NOT assignment!)
func(a); // Copy constructor (pass by value)
return a; // Copy constructor (return by value)
// SHALLOW COPY (default, dangerous with pointers)
class Shallow {
int* _ptr;
public:
Shallow(const Shallow& other) {
_ptr = other._ptr; // Both point to same memory!
}
};
// DEEP COPY (correct for pointers)
class Deep {
int* _ptr;
public:
Deep(const Deep& other) {
_ptr = new int(*other._ptr); // New memory, copy value
}
};

Assigns the value of one EXISTING object to another EXISTING object.

class Sample {
public:
Sample& operator=(const Sample& other) {
std::cout << "Copy assignment operator called" << std::endl;
// 1. Check for self-assignment
if (this != &other) {
// 2. Clean up existing resources
delete _data;
// 3. Copy values
_value = other._value;
_data = new int(*other._data);
}
// 4. Return *this for chaining: a = b = c;
return *this;
}
};
Sample a;
Sample b;
b = a; // Assignment operator (b already exists)
Sample a;
a = a; // Self-assignment - would be bug without check!
// Without check:
// 1. delete _data; // Free the memory
// 2. _data = new int(*other._data); // other._data is already deleted!

class Fixed {
private:
int _rawValue;
static const int _fractionalBits = 8;
public:
// Default constructor
Fixed() : _rawValue(0) {
std::cout << "Default constructor called" << std::endl;
}
// Copy constructor
Fixed(const Fixed& other) : _rawValue(other._rawValue) {
std::cout << "Copy constructor called" << std::endl;
}
// Copy assignment operator
Fixed& operator=(const Fixed& other) {
std::cout << "Copy assignment operator called" << std::endl;
if (this != &other)
_rawValue = other._rawValue;
return *this;
}
// Destructor
~Fixed() {
std::cout << "Destructor called" << std::endl;
}
// Getters/Setters
int getRawBits() const {
std::cout << "getRawBits member function called" << std::endl;
return _rawValue;
}
void setRawBits(int const raw) {
_rawValue = raw;
}
};

Defining custom behavior for operators (+, -, *, /, ==, <, <<, etc.) when used with your class.

// As member function
ReturnType operator@(parameters);
// As non-member function (friend or uses public interface)
ReturnType operator@(LeftType, RightType);
class Fixed {
public:
bool operator>(const Fixed& other) const {
return _rawValue > other._rawValue;
}
bool operator<(const Fixed& other) const {
return _rawValue < other._rawValue;
}
bool operator>=(const Fixed& other) const {
return _rawValue >= other._rawValue;
}
bool operator<=(const Fixed& other) const {
return _rawValue <= other._rawValue;
}
bool operator==(const Fixed& other) const {
return _rawValue == other._rawValue;
}
bool operator!=(const Fixed& other) const {
return _rawValue != other._rawValue;
}
};
class Fixed {
public:
Fixed operator+(const Fixed& other) const {
Fixed result;
result._rawValue = _rawValue + other._rawValue;
return result;
}
Fixed operator-(const Fixed& other) const {
Fixed result;
result._rawValue = _rawValue - other._rawValue;
return result;
}
Fixed operator*(const Fixed& other) const {
Fixed result;
// For fixed-point: (a * b) >> fractionalBits
result._rawValue = (_rawValue * other._rawValue) >> _fractionalBits;
return result;
}
Fixed operator/(const Fixed& other) const {
Fixed result;
// For fixed-point: (a << fractionalBits) / b
result._rawValue = (_rawValue << _fractionalBits) / other._rawValue;
return result;
}
};
class Fixed {
public:
// Pre-increment: ++a
Fixed& operator++() {
_rawValue++;
return *this;
}
// Post-increment: a++
Fixed operator++(int) { // int parameter is just a marker
Fixed temp(*this); // Save current value
_rawValue++; // Increment
return temp; // Return old value
}
// Pre-decrement: --a
Fixed& operator--() {
_rawValue--;
return *this;
}
// Post-decrement: a--
Fixed operator--(int) {
Fixed temp(*this);
_rawValue--;
return temp;
}
};

Understanding why pre-increment returns by reference and post-increment returns by value:

┌─────────────────────────────────────────────────────────────────┐
│ Pre-Increment (++a) vs Post-Increment (a++) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ PRE-INCREMENT (++a): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Increment the value │ │
│ │ 2. Return *this (the object itself) │ │
│ │ │ │
│ │ Fixed& operator++() { ┌─────┐ │ │
│ │ _rawValue++; ────→ │ 6 │ a (after) │ │
│ │ return *this; └─────┘ │ │
│ │ } ↑ │ │
│ │ Return by reference │ │
│ │ (returns the object) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ POST-INCRECREMENT (a++): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Save old value in temp │ │
│ │ 2. Increment the value │ │
│ │ 3. Return temp (the OLD value) │ │
│ │ │ │
│ │ Fixed operator++(int) { ┌─────┐ │ │
│ │ Fixed temp(*this); │ 5 │ temp (old) │ │
│ │ _rawValue++; └─────┘ │ │
│ │ return temp; ────→ ↑ │ │
│ │ } Return by value │ │
│ │ (returns a COPY) │ │
│ │ ┌─────┐ │ │
│ │ │ 6 │ a (after) │ │
│ │ └─────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ KEY DIFFERENCES: │
│ ┌────────────────┬──────────────────┬──────────────────┐ │
│ │ │ Pre (++a) │ Post (a++) │ │
│ ├────────────────┼──────────────────┼──────────────────┤ │
│ │ Return type │ Fixed& (ref) │ Fixed (value) │ │
│ │ Returns │ The object │ A copy │ │
│ │ Can chain? │ Yes: ++++a │ No: a++++ │ │
│ │ Performance │ Faster (no copy) │ Slower (copy) │ │
│ │ Usage │ Preferred │ When old value │ │
│ │ │ │ needed │ │
│ └────────────────┴──────────────────┴──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Best Practice: Use pre-increment (++a) when you don’t need the old value—it’s more efficient because it avoids creating a temporary copy.

// Must be non-member function (ostream is on the left)
std::ostream& operator<<(std::ostream& os, const Fixed& fixed) {
os << fixed.toFloat();
return os;
}
// Usage
Fixed a(42.42f);
std::cout << a << std::endl; // Prints: 42.4219

A way to represent decimal numbers using integers, with a fixed number of bits for the fractional part.

Integer: 42
Binary: 00101010.00000000
^^^^^^^^ ^^^^^^^^
Integer Fraction
To convert:
- Int to Fixed: int << 8
- Fixed to Int: fixed >> 8
- Float to Fixed: float * 256 (then round)
- Fixed to Float: fixed / 256.0f
class Fixed {
private:
int _rawValue;
static const int _fractionalBits = 8;
public:
// From int
Fixed(const int value) : _rawValue(value << _fractionalBits) {}
// From float
Fixed(const float value)
: _rawValue(roundf(value * (1 << _fractionalBits))) {}
// To int
int toInt() const {
return _rawValue >> _fractionalBits;
}
// To float
float toFloat() const {
return (float)_rawValue / (1 << _fractionalBits);
}
};
  • Faster than floating-point on some hardware
  • Predictable precision
  • No floating-point rounding errors for exact values
  • Used in: graphics, audio, embedded systems

class Fixed {
public:
// Non-const version
static Fixed& min(Fixed& a, Fixed& b) {
return (a < b) ? a : b;
}
// Const version
static const Fixed& min(const Fixed& a, const Fixed& b) {
return (a < b) ? a : b;
}
// Non-const version
static Fixed& max(Fixed& a, Fixed& b) {
return (a > b) ? a : b;
}
// Const version
static const Fixed& max(const Fixed& a, const Fixed& b) {
return (a > b) ? a : b;
}
};
// Usage
Fixed a(2.0f), b(3.0f);
std::cout << Fixed::max(a, b) << std::endl; // 3

Create a Fixed class that represents fixed-point numbers with these four required members (OCF):

  1. Default constructor - Creates a Fixed with value 0
  2. Copy constructor - Creates a Fixed from another Fixed
  3. Copy assignment operator - Assigns one Fixed to another
  4. Destructor - Cleans up (prints message for this exercise)

Plus basic getters/setters for the raw bits value.

Step 1 - Understand what OCF means

These four functions control how objects are:

  • Created from nothing (default constructor)
  • Created from another object (copy constructor)
  • Assigned from another object (assignment operator)
  • Destroyed (destructor)

Step 2 - Understand fixed-point representation

A fixed-point number stores fractional values using integers:

  • 8 bits for fractional part (right of decimal)
  • Remaining bits for integer part (left of decimal)
  • Value = _rawValue / 256 (since 2^8 = 256)

Step 3 - Print messages to show when each is called

The subject requires printing messages so you can see the order of calls.

Stage 1 - Class declaration:

Fixed.hpp
#ifndef FIXED_HPP
#define FIXED_HPP
class Fixed {
private:
int _rawValue;
static const int _fractionalBits = 8;
public:
Fixed(); // Default constructor
Fixed(const Fixed& other); // Copy constructor
Fixed& operator=(const Fixed& other); // Copy assignment
~Fixed(); // Destructor
int getRawBits() const;
void setRawBits(int const raw);
};
#endif

Stage 2 - Implementation:

Fixed.cpp
#include "Fixed.hpp"
#include <iostream>
Fixed::Fixed() : _rawValue(0) {
std::cout << "Default constructor called" << std::endl;
}
Fixed::Fixed(const Fixed& other) : _rawValue(other._rawValue) {
std::cout << "Copy constructor called" << std::endl;
}
Fixed& Fixed::operator=(const Fixed& other) {
std::cout << "Copy assignment operator called" << std::endl;
if (this != &other)
_rawValue = other._rawValue;
return *this;
}
Fixed::~Fixed() {
std::cout << "Destructor called" << std::endl;
}
int Fixed::getRawBits() const {
std::cout << "getRawBits member function called" << std::endl;
return _rawValue;
}
void Fixed::setRawBits(int const raw) {
_rawValue = raw;
}
LineCodeWhy
Fixed(const Fixed& other)Copy constructor signatureTakes const reference to source
: _rawValue(other._rawValue)Initializer listCopy the value directly
Fixed& operator=(...)Assignment returns referenceEnables chaining: a = b = c
if (this != &other)Self-assignment checkPrevents a = a from breaking
return *thisReturn this objectEnables chaining
static const intClass constantShared by all instances

1. Confusing copy constructor vs assignment

Fixed a;
Fixed b(a); // Copy constructor - b is being CREATED
Fixed c = a; // ALSO copy constructor - c is being CREATED (initialization syntax)
Fixed d;
d = a; // Assignment operator - d already EXISTS

2. Forgetting self-assignment check

// WRONG - breaks if a = a
Fixed& operator=(const Fixed& other) {
_rawValue = other._rawValue;
return *this;
}
// RIGHT - safe for self-assignment
Fixed& operator=(const Fixed& other) {
if (this != &other)
_rawValue = other._rawValue;
return *this;
}

3. Not returning *this from assignment

// WRONG - can't chain assignments
void operator=(const Fixed& other) {
_rawValue = other._rawValue;
}
// RIGHT - enables a = b = c
Fixed& operator=(const Fixed& other) {
// ...
return *this;
}
Terminal window
# Compile and run
c++ -Wall -Wextra -Werror *.cpp -o fixed
# Expected output for basic test:
# Default constructor called
# Copy constructor called
# Copy assignment operator called
# (followed by destructor calls in reverse order)
Fixed.hpp
#ifndef FIXED_HPP
#define FIXED_HPP
class Fixed {
private:
int _rawValue;
static const int _fractionalBits = 8;
public:
Fixed();
Fixed(const Fixed& other);
Fixed& operator=(const Fixed& other);
~Fixed();
int getRawBits() const;
void setRawBits(int const raw);
};
#endif

Exercise 01: Toward a More Useful Fixed-Point

Section titled “Exercise 01: Toward a More Useful Fixed-Point”

Extend the Fixed class with:

  • Constructor from int: Convert integer to fixed-point
  • Constructor from float: Convert float to fixed-point
  • toInt(): Convert fixed-point back to integer
  • toFloat(): Convert fixed-point back to float
  • operator<<: Stream insertion for printing

Step 1 - Understand the math

With 8 fractional bits:

  • int → fixed: Shift left by 8 (multiply by 256)
  • fixed → int: Shift right by 8 (divide by 256)
  • float → fixed: Multiply by 256, then round
  • fixed → float: Divide by 256.0

Step 2 - Why these conversions?

Example: 42.42 in fixed-point
42.42 * 256 = 10859.52 → round to 10860
10860 / 256 = 42.421875 (close to original)

Step 3 - Stream operator is NOT a member

std::cout << fixed means ostream is on the left. We can’t modify ostream, so operator<< must be a non-member function.

Stage 1 - Conversion constructors:

Fixed.cpp
// From int: shift left by fractional bits
Fixed::Fixed(const int value)
: _rawValue(value << _fractionalBits) {
std::cout << "Int constructor called" << std::endl;
}
// From float: multiply by 2^8 and round
Fixed::Fixed(const float value)
: _rawValue(roundf(value * (1 << _fractionalBits))) {
std::cout << "Float constructor called" << std::endl;
}

Stage 2 - Conversion functions:

Fixed.cpp
// To int: shift right (loses fractional part)
int Fixed::toInt() const {
return _rawValue >> _fractionalBits;
}
// To float: divide by 2^8
float Fixed::toFloat() const {
return (float)_rawValue / (1 << _fractionalBits);
}

Stage 3 - Stream operator:

Fixed.cpp
// Non-member function
std::ostream& operator<<(std::ostream& os, const Fixed& fixed) {
os << fixed.toFloat();
return os;
}
LineCodeWhy
value << _fractionalBitsShift left 8 bitsEquivalent to value * 256
roundf(value * ...)Round to nearest intFloat conversion needs rounding
1 << _fractionalBitsEquals 2562^8 = 256
_rawValue >> _fractionalBitsShift right 8 bitsEquivalent to integer division by 256
(float)_rawValue / ...Cast to float firstAvoid integer division!
return osReturn the streamEnables chaining: cout << a << b

1. Forgetting roundf() for float conversion

// WRONG - truncates, loses precision
Fixed::Fixed(const float value)
: _rawValue(value * (1 << _fractionalBits)) {}
// RIGHT - rounds to nearest
Fixed::Fixed(const float value)
: _rawValue(roundf(value * (1 << _fractionalBits))) {}

2. Integer division in toFloat()

// WRONG - integer division gives 0 for small values!
float Fixed::toFloat() const {
return _rawValue / (1 << _fractionalBits);
}
// RIGHT - cast to float first
float Fixed::toFloat() const {
return (float)_rawValue / (1 << _fractionalBits);
}

3. Making operator<< a member function

// WRONG - can't be member (ostream on left side)
class Fixed {
std::ostream& operator<<(std::ostream& os) const;
};
// RIGHT - non-member function
std::ostream& operator<<(std::ostream& os, const Fixed& fixed);
int main() {
Fixed a;
Fixed const b(10); // From int
Fixed const c(42.42f); // From float
Fixed const d(b); // Copy
std::cout << "a is " << a << std::endl; // 0
std::cout << "b is " << b << std::endl; // 10
std::cout << "c is " << c << std::endl; // 42.4219 (close to 42.42)
std::cout << "d is " << d << std::endl; // 10
std::cout << "b as int: " << b.toInt() << std::endl; // 10
std::cout << "c as int: " << c.toInt() << std::endl; // 42
return 0;
}
Fixed.hpp
#ifndef FIXED_HPP
#define FIXED_HPP
#include <iostream>
class Fixed {
private:
int _rawValue;
static const int _fractionalBits = 8;
public:
Fixed();
Fixed(const int value);
Fixed(const float value);
Fixed(const Fixed& other);
Fixed& operator=(const Fixed& other);
~Fixed();
int getRawBits() const;
void setRawBits(int const raw);
float toFloat() const;
int toInt() const;
};
std::ostream& operator<<(std::ostream& os, const Fixed& fixed);
#endif

Add operator overloading to the Fixed class:

  • 6 comparison operators: >, <, >=, <=, ==, !=
  • 4 arithmetic operators: +, -, *, /
  • 4 increment/decrement: pre/post ++, pre/post --
  • 4 static min/max functions: for Fixed references and const references

Step 1 - Comparison operators

Compare raw bits directly - that’s the beauty of fixed-point!

Step 2 - Arithmetic operators

Return a new Fixed object (not modify existing).

Step 3 - Increment: Pre vs Post

  • Pre-increment ++a: Modify and return reference
  • Post-increment a++: Save copy, modify, return old copy

Step 4 - The smallest increment

Adding 1 to raw bits = adding 1/256 = 0.00390625 to the value.

Stage 1 - Comparison operators:

Fixed.cpp
bool Fixed::operator>(const Fixed& other) const {
return _rawValue > other._rawValue;
}
bool Fixed::operator<(const Fixed& other) const {
return _rawValue < other._rawValue;
}
bool Fixed::operator>=(const Fixed& other) const {
return _rawValue >= other._rawValue;
}
bool Fixed::operator<=(const Fixed& other) const {
return _rawValue <= other._rawValue;
}
bool Fixed::operator==(const Fixed& other) const {
return _rawValue == other._rawValue;
}
bool Fixed::operator!=(const Fixed& other) const {
return _rawValue != other._rawValue;
}

Stage 2 - Arithmetic operators:

Fixed.cpp
Fixed Fixed::operator+(const Fixed& other) const {
return Fixed(toFloat() + other.toFloat());
}
Fixed Fixed::operator-(const Fixed& other) const {
return Fixed(toFloat() - other.toFloat());
}
Fixed Fixed::operator*(const Fixed& other) const {
return Fixed(toFloat() * other.toFloat());
}
Fixed Fixed::operator/(const Fixed& other) const {
return Fixed(toFloat() / other.toFloat());
}

Stage 3 - Increment/decrement:

Fixed.cpp
// Pre-increment: ++a
Fixed& Fixed::operator++() {
_rawValue++;
return *this;
}
// Post-increment: a++
Fixed Fixed::operator++(int) {
Fixed temp(*this); // Save current value
_rawValue++; // Increment
return temp; // Return OLD value
}
// Pre-decrement: --a
Fixed& Fixed::operator--() {
_rawValue--;
return *this;
}
// Post-decrement: a--
Fixed Fixed::operator--(int) {
Fixed temp(*this);
_rawValue--;
return temp;
}

Stage 4 - Static min/max:

Fixed.cpp
Fixed& Fixed::min(Fixed& a, Fixed& b) {
return (a < b) ? a : b;
}
const Fixed& Fixed::min(const Fixed& a, const Fixed& b) {
return (a < b) ? a : b;
}
Fixed& Fixed::max(Fixed& a, Fixed& b) {
return (a > b) ? a : b;
}
const Fixed& Fixed::max(const Fixed& a, const Fixed& b) {
return (a > b) ? a : b;
}
LineCodeWhy
operator++(int)Post-increment signatureint parameter distinguishes from pre
Fixed temp(*this)Save current valuePost-increment returns old value
return *thisReturn referencePre-increment returns modified object
return tempReturn copyPost-increment returns saved copy
static Fixed& min(...)Static functionCalled as Fixed::min(a, b)
Two min overloadsFor const/non-constPreserves const-correctness

1. Wrong return types for pre/post increment

// WRONG - post should return copy, not reference
Fixed& operator++(int) {
// ...
}
// RIGHT
Fixed& operator++(); // Pre returns reference
Fixed operator++(int); // Post returns copy

2. Not understanding the smallest increment

Fixed a(0);
a++; // a is now 0.00390625 (1/256)
// NOT 1.0!

3. Forgetting both const and non-const versions of min/max

// Need both:
static Fixed& min(Fixed& a, Fixed& b); // For non-const
static const Fixed& min(const Fixed& a, const Fixed& b); // For const
int main() {
Fixed a;
Fixed const b(Fixed(5.05f) * Fixed(2));
std::cout << a << std::endl; // 0
std::cout << ++a << std::endl; // 0.00390625
std::cout << a << std::endl; // 0.00390625
std::cout << a++ << std::endl; // 0.00390625
std::cout << a << std::endl; // 0.0078125
std::cout << b << std::endl; // 10.1016
std::cout << Fixed::max(a, b) << std::endl; // 10.1016
return 0;
}
Fixed.hpp (operators section)
// Comparison
bool operator>(const Fixed& other) const;
bool operator<(const Fixed& other) const;
bool operator>=(const Fixed& other) const;
bool operator<=(const Fixed& other) const;
bool operator==(const Fixed& other) const;
bool operator!=(const Fixed& other) const;
// Arithmetic
Fixed operator+(const Fixed& other) const;
Fixed operator-(const Fixed& other) const;
Fixed operator*(const Fixed& other) const;
Fixed operator/(const Fixed& other) const;
// Increment/Decrement
Fixed& operator++();
Fixed operator++(int);
Fixed& operator--();
Fixed operator--(int);
// Static min/max
static Fixed& min(Fixed& a, Fixed& b);
static const Fixed& min(const Fixed& a, const Fixed& b);
static Fixed& max(Fixed& a, Fixed& b);
static const Fixed& max(const Fixed& a, const Fixed& b);

Exercise 03: BSP (Binary Space Partitioning)

Section titled “Exercise 03: BSP (Binary Space Partitioning)”

Implement a function to test if a point is inside a triangle:

bool bsp(Point const a, Point const b, Point const c, Point const point);
  • Returns true if point is strictly inside the triangle
  • Returns false if point is on an edge or vertex

You must also create a Point class with const x and y attributes.

Step 1 - Understand the algorithm (Cross Product Method)

For each edge of the triangle:

  1. Compute the cross product of (edge vector) x (point-to-vertex vector)
  2. If all three cross products have the same sign, point is inside
  3. If any cross product is zero, point is on an edge

Step 2 - Design the Point class

Key constraint: x and y are const - can’t be changed after construction.

Step 3 - The cross product formula

For vectors from P1 to P2 and P1 to P3:

cross = (P2.x - P1.x) * (P3.y - P1.y) - (P2.y - P1.y) * (P3.x - P1.x)

Stage 1 - Point class:

Point.hpp
#ifndef POINT_HPP
#define POINT_HPP
#include "Fixed.hpp"
class Point {
private:
Fixed const _x;
Fixed const _y;
public:
Point();
Point(const float x, const float y);
Point(const Point& other);
Point& operator=(const Point& other);
~Point();
Fixed getX() const;
Fixed getY() const;
};
bool bsp(Point const a, Point const b, Point const c, Point const point);
#endif

Stage 2 - Point implementation:

Point.cpp
#include "Point.hpp"
Point::Point() : _x(0), _y(0) {}
Point::Point(const float x, const float y) : _x(x), _y(y) {}
Point::Point(const Point& other) : _x(other._x), _y(other._y) {}
// Assignment can't modify const members - effectively does nothing
Point& Point::operator=(const Point& other) {
(void)other;
return *this;
}
Point::~Point() {}
Fixed Point::getX() const { return _x; }
Fixed Point::getY() const { return _y; }

Stage 3 - BSP algorithm:

bsp.cpp
#include "Point.hpp"
// Cross product tells us which side of a line a point is on
static Fixed crossProduct(Point const& p1, Point const& p2, Point const& p3) {
return (p2.getX() - p1.getX()) * (p3.getY() - p1.getY())
- (p2.getY() - p1.getY()) * (p3.getX() - p1.getX());
}
bool bsp(Point const a, Point const b, Point const c, Point const point) {
// Compute cross products for each edge
Fixed d1 = crossProduct(a, b, point);
Fixed d2 = crossProduct(b, c, point);
Fixed d3 = crossProduct(c, a, point);
// Check if on any edge (cross product = 0)
if (d1 == Fixed(0) || d2 == Fixed(0) || d3 == Fixed(0))
return false;
// Check if all same sign (all positive OR all negative)
bool allNegative = (d1 < Fixed(0)) && (d2 < Fixed(0)) && (d3 < Fixed(0));
bool allPositive = (d1 > Fixed(0)) && (d2 > Fixed(0)) && (d3 > Fixed(0));
return allNegative || allPositive;
}
LineCodeWhy
Fixed const _xConst memberX coordinate can’t change
(void)otherSuppress warningAssignment can’t do anything useful
crossProduct(a, b, point)Edge AB to pointSign tells us which side
d1 == Fixed(0)On edge checkZero cross = collinear = on edge
allNegative || allPositiveInside checkSame sign means same side of all edges

1. Returning true for points on edges

// WRONG - subject says edge/vertex = false
if (d1 == Fixed(0))
return true;
// RIGHT
if (d1 == Fixed(0))
return false;

2. Trying to modify const members in assignment

// WRONG - won't compile, _x and _y are const
Point& Point::operator=(const Point& other) {
_x = other._x; // Error!
_y = other._y; // Error!
return *this;
}
// RIGHT - acknowledge it does nothing useful
Point& Point::operator=(const Point& other) {
(void)other;
return *this;
}

3. Wrong cross product formula

// WRONG - wrong order of operations
(p2.getX() - p1.getX()) * (p2.getY() - p1.getY())
// RIGHT
(p2.getX() - p1.getX()) * (p3.getY() - p1.getY())
- (p2.getY() - p1.getY()) * (p3.getX() - p1.getX())
int main() {
// Triangle with vertices at (0,0), (10,0), (5,10)
Point a(0.0f, 0.0f);
Point b(10.0f, 0.0f);
Point c(5.0f, 10.0f);
// Test cases
Point inside(5.0f, 3.0f); // Inside
Point onEdge(5.0f, 0.0f); // On edge AB
Point onVertex(0.0f, 0.0f); // On vertex A
Point outside(15.0f, 5.0f); // Outside
std::cout << "Inside: " << bsp(a, b, c, inside) << std::endl; // 1
std::cout << "On edge: " << bsp(a, b, c, onEdge) << std::endl; // 0
std::cout << "On vertex: " << bsp(a, b, c, onVertex) << std::endl; // 0
std::cout << "Outside: " << bsp(a, b, c, outside) << std::endl; // 0
return 0;
}
bsp.cpp
#include "Point.hpp"
static Fixed crossProduct(Point const& p1, Point const& p2, Point const& p3) {
return (p2.getX() - p1.getX()) * (p3.getY() - p1.getY())
- (p2.getY() - p1.getY()) * (p3.getX() - p1.getX());
}
bool bsp(Point const a, Point const b, Point const c, Point const point) {
Fixed d1 = crossProduct(a, b, point);
Fixed d2 = crossProduct(b, c, point);
Fixed d3 = crossProduct(c, a, point);
if (d1 == Fixed(0) || d2 == Fixed(0) || d3 == Fixed(0))
return false;
bool allNegative = (d1 < Fixed(0)) && (d2 < Fixed(0)) && (d3 < Fixed(0));
bool allPositive = (d1 > Fixed(0)) && (d2 > Fixed(0)) && (d3 > Fixed(0));
return allNegative || allPositive;
}

class ClassName {
public:
ClassName(); // Default constructor
ClassName(const ClassName& other); // Copy constructor
ClassName& operator=(const ClassName& other); // Assignment operator
~ClassName(); // Destructor
};
OperatorMember?Signature
+, -, *, /YesT operator+(const T&) const
==, !=, <, >Yesbool operator==(const T&) const
++, -- (pre)YesT& operator++()
++, -- (post)YesT operator++(int)
<<Noostream& operator<<(ostream&, const T&)
// 8 fractional bits
int_to_fixed: value << 8
fixed_to_int: value >> 8
float_to_fixed: roundf(value * 256)
fixed_to_float: value / 256.0f

Continue your C++ journey:

Visit the Glossary for definitions of: