Module 02 Tutorial
Prerequisites: Review Module 02 Concepts first.
Module 02 introduces the Orthodox Canonical Form (OCF) - the four functions every C++ class should have. You’ll also learn fixed-point arithmetic and operator overloading.
Exercise 00: Orthodox Canonical Form
Section titled “Exercise 00: Orthodox Canonical Form”Subject Analysis
Section titled “Subject Analysis”Create a Fixed class that represents fixed-point numbers with these four required members (OCF):
- Default constructor - Creates a Fixed with value 0
- Copy constructor - Creates a Fixed from another Fixed
- Copy assignment operator - Assigns one Fixed to another
- Destructor - Cleans up (prints message for this exercise)
Plus basic getters/setters for the raw bits value.
Approach Strategy
Section titled “Approach Strategy”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.
Progressive Code Building
Section titled “Progressive Code Building”Stage 1 - Class declaration:
#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);};
#endifStage 2 - Implementation:
#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;}Line-by-Line Explanation
Section titled “Line-by-Line Explanation”| Line | Code | Why |
|---|---|---|
Fixed(const Fixed& other) | Copy constructor signature | Takes const reference to source |
: _rawValue(other._rawValue) | Initializer list | Copy the value directly |
Fixed& operator=(...) | Assignment returns reference | Enables chaining: a = b = c |
if (this != &other) | Self-assignment check | Prevents a = a from breaking |
return *this | Return this object | Enables chaining |
static const int | Class constant | Shared by all instances |
Common Pitfalls
Section titled “Common Pitfalls”1. Confusing copy constructor vs assignment
Fixed a;Fixed b(a); // Copy constructor - b is being CREATEDFixed c = a; // ALSO copy constructor - c is being CREATED (initialization syntax)
Fixed d;d = a; // Assignment operator - d already EXISTS2. Forgetting self-assignment check
// WRONG - breaks if a = aFixed& operator=(const Fixed& other) { _rawValue = other._rawValue; return *this;}
// RIGHT - safe for self-assignmentFixed& operator=(const Fixed& other) { if (this != &other) _rawValue = other._rawValue; return *this;}3. Not returning *this from assignment
// WRONG - can't chain assignmentsvoid operator=(const Fixed& other) { _rawValue = other._rawValue;}
// RIGHT - enables a = b = cFixed& operator=(const Fixed& other) { // ... return *this;}Testing Tips
Section titled “Testing Tips”# Compile and runc++ -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)Final Code
Section titled “Final Code”#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);};
#endifExercise 01: Toward a More Useful Fixed-Point
Section titled “Exercise 01: Toward a More Useful Fixed-Point”Subject Analysis
Section titled “Subject Analysis”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
Approach Strategy
Section titled “Approach Strategy”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-point42.42 * 256 = 10859.52 → round to 1086010860 / 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.
Progressive Code Building
Section titled “Progressive Code Building”Stage 1 - Conversion constructors:
// From int: shift left by fractional bitsFixed::Fixed(const int value) : _rawValue(value << _fractionalBits) { std::cout << "Int constructor called" << std::endl;}
// From float: multiply by 2^8 and roundFixed::Fixed(const float value) : _rawValue(roundf(value * (1 << _fractionalBits))) { std::cout << "Float constructor called" << std::endl;}Stage 2 - Conversion functions:
// To int: shift right (loses fractional part)int Fixed::toInt() const { return _rawValue >> _fractionalBits;}
// To float: divide by 2^8float Fixed::toFloat() const { return (float)_rawValue / (1 << _fractionalBits);}Stage 3 - Stream operator:
// Non-member functionstd::ostream& operator<<(std::ostream& os, const Fixed& fixed) { os << fixed.toFloat(); return os;}Line-by-Line Explanation
Section titled “Line-by-Line Explanation”| Line | Code | Why |
|---|---|---|
value << _fractionalBits | Shift left 8 bits | Equivalent to value * 256 |
roundf(value * ...) | Round to nearest int | Float conversion needs rounding |
1 << _fractionalBits | Equals 256 | 2^8 = 256 |
_rawValue >> _fractionalBits | Shift right 8 bits | Equivalent to integer division by 256 |
(float)_rawValue / ... | Cast to float first | Avoid integer division! |
return os | Return the stream | Enables chaining: cout << a << b |
Common Pitfalls
Section titled “Common Pitfalls”1. Forgetting roundf() for float conversion
// WRONG - truncates, loses precisionFixed::Fixed(const float value) : _rawValue(value * (1 << _fractionalBits)) {}
// RIGHT - rounds to nearestFixed::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 firstfloat 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 functionstd::ostream& operator<<(std::ostream& os, const Fixed& fixed);Testing Tips
Section titled “Testing Tips”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;}Final Code
Section titled “Final Code”#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);
#endifExercise 02: Now We’re Talking
Section titled “Exercise 02: Now We’re Talking”Subject Analysis
Section titled “Subject Analysis”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
Approach Strategy
Section titled “Approach Strategy”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.
Progressive Code Building
Section titled “Progressive Code Building”Stage 1 - Comparison operators:
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 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:
// Pre-increment: ++aFixed& 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: --aFixed& Fixed::operator--() { _rawValue--; return *this;}
// Post-decrement: a--Fixed Fixed::operator--(int) { Fixed temp(*this); _rawValue--; return temp;}Stage 4 - Static min/max:
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;}Line-by-Line Explanation
Section titled “Line-by-Line Explanation”| Line | Code | Why |
|---|---|---|
operator++(int) | Post-increment signature | int parameter distinguishes from pre |
Fixed temp(*this) | Save current value | Post-increment returns old value |
return *this | Return reference | Pre-increment returns modified object |
return temp | Return copy | Post-increment returns saved copy |
static Fixed& min(...) | Static function | Called as Fixed::min(a, b) |
| Two min overloads | For const/non-const | Preserves const-correctness |
Common Pitfalls
Section titled “Common Pitfalls”1. Wrong return types for pre/post increment
// WRONG - post should return copy, not referenceFixed& operator++(int) { // ...}
// RIGHTFixed& operator++(); // Pre returns referenceFixed operator++(int); // Post returns copy2. 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-conststatic const Fixed& min(const Fixed& a, const Fixed& b); // For constTesting Tips
Section titled “Testing Tips”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;}Final Code
Section titled “Final Code”// Comparisonbool 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;
// ArithmeticFixed operator+(const Fixed& other) const;Fixed operator-(const Fixed& other) const;Fixed operator*(const Fixed& other) const;Fixed operator/(const Fixed& other) const;
// Increment/DecrementFixed& operator++();Fixed operator++(int);Fixed& operator--();Fixed operator--(int);
// Static min/maxstatic 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)”Subject Analysis
Section titled “Subject Analysis”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
trueif point is strictly inside the triangle - Returns
falseif point is on an edge or vertex
You must also create a Point class with const x and y attributes.
Approach Strategy
Section titled “Approach Strategy”Step 1 - Understand the algorithm (Cross Product Method)
For each edge of the triangle:
- Compute the cross product of (edge vector) x (point-to-vertex vector)
- If all three cross products have the same sign, point is inside
- 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)Progressive Code Building
Section titled “Progressive Code Building”Stage 1 - Point class:
#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);
#endifStage 2 - Point implementation:
#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 nothingPoint& 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:
#include "Point.hpp"
// Cross product tells us which side of a line a point is onstatic 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;}Line-by-Line Explanation
Section titled “Line-by-Line Explanation”| Line | Code | Why |
|---|---|---|
Fixed const _x | Const member | X coordinate can’t change |
(void)other | Suppress warning | Assignment can’t do anything useful |
crossProduct(a, b, point) | Edge AB to point | Sign tells us which side |
d1 == Fixed(0) | On edge check | Zero cross = collinear = on edge |
allNegative || allPositive | Inside check | Same sign means same side of all edges |
Common Pitfalls
Section titled “Common Pitfalls”1. Returning true for points on edges
// WRONG - subject says edge/vertex = falseif (d1 == Fixed(0)) return true;
// RIGHTif (d1 == Fixed(0)) return false;2. Trying to modify const members in assignment
// WRONG - won't compile, _x and _y are constPoint& Point::operator=(const Point& other) { _x = other._x; // Error! _y = other._y; // Error! return *this;}
// RIGHT - acknowledge it does nothing usefulPoint& 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())Testing Tips
Section titled “Testing Tips”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;}Final Code
Section titled “Final Code”#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;}