Module 02: Operator Overloading and Orthodox Canonical Form
Key Concepts:
- Orthodox Canonical Form (OCF)
- Copy constructor
- Copy assignment operator
- Operator overloading
- Fixed-point numbers
Why This Matters
Section titled “Why This Matters”This module introduces two critical concepts: OCF (Orthodox Canonical Form) and operator overloading.
Why OCF?
Section titled “Why OCF?”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.
Why Operator Overloading?
Section titled “Why Operator Overloading?”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.
Why Function Overloading Matters
Section titled “Why Function Overloading Matters”In C, if you wanted functions to work with different types, you had to give them different names:
// C approach - different names for different typesint 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 oneint 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.
Understanding Operators in This Module
Section titled “Understanding Operators in This Module”This module teaches you how to define custom behavior for operators. Here’s what the new syntax means:
The = Operator (Assignment)
Section titled “The = Operator (Assignment)”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
The & Operator (Address-of in Self-Check)
Section titled “The & Operator (Address-of in Self-Check)”if (this != &other) { // Check for self-assignment&othergets the memory address of theotherobjectthisis a pointer to the current objectthis != &othercompares two addresses to check if they’re the same object- This prevents bugs when someone writes
a = a;
The * Operator (Dereference for Return)
Section titled “The * Operator (Dereference for Return)”return *this; // Return the current object (not the pointer!)*thisdereferences thethispointer to get the actual object- Assignment operators return
*thisso you can chain assignments:a = b = c; - Without the
*, you’d return a pointer, which wouldn’t allow chaining
Overloading Operators: The Syntax
Section titled “Overloading Operators: The Syntax”// 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 isstd::ostream/std::istream(not your class)
The ++ and -- Operators
Section titled “The ++ and -- Operators”// Pre-increment: ++aFixed& 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!)<< 8multiplies by 2^8 (256) - shifts bits to the left>> 8divides 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!
Copy Constructor with & (const Reference)
Section titled “Copy Constructor with & (const Reference)”Fixed(const Fixed& other); // Copy constructor&after the type means “reference to”constmeans “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!)
1. Orthodox Canonical Form (OCF)
Section titled “1. Orthodox Canonical Form (OCF)”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.
The Four Required Members
Section titled “The Four Required Members”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};OCF Pattern Diagram
Section titled “OCF Pattern Diagram”┌─────────────────────────────────────────────────────────────────┐│ 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.
Why OCF Matters
Section titled “Why OCF Matters”Without proper OCF, classes with dynamic memory will have bugs:
// BAD: No copy constructor or assignment operatorclass 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!2. Copy Constructor
Section titled “2. Copy Constructor”Purpose
Section titled “Purpose”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); }};When It’s Called
Section titled “When It’s Called”Sample a;Sample b(a); // Copy constructorSample c = a; // Copy constructor (NOT assignment!)func(a); // Copy constructor (pass by value)return a; // Copy constructor (return by value)Shallow vs Deep Copy
Section titled “Shallow vs Deep Copy”// 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 }};3. Copy Assignment Operator
Section titled “3. Copy Assignment Operator”Purpose
Section titled “Purpose”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; }};When It’s Called
Section titled “When It’s Called”Sample a;Sample b;b = a; // Assignment operator (b already exists)The Self-Assignment Check
Section titled “The Self-Assignment Check”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!4. Complete OCF Example
Section titled “4. Complete OCF Example”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; }};5. Operator Overloading
Section titled “5. Operator Overloading”What is Operator Overloading?
Section titled “What is Operator Overloading?”Defining custom behavior for operators (+, -, *, /, ==, <, <<, etc.) when used with your class.
Syntax
Section titled “Syntax”// As member functionReturnType operator@(parameters);
// As non-member function (friend or uses public interface)ReturnType operator@(LeftType, RightType);Comparison Operators
Section titled “Comparison Operators”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; }};Arithmetic Operators
Section titled “Arithmetic Operators”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; }};Increment/Decrement Operators
Section titled “Increment/Decrement Operators”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; }};Pre vs Post Increment Return Types
Section titled “Pre vs Post Increment Return Types”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.
Stream Insertion Operator (<<)
Section titled “Stream Insertion Operator (<<)”// Must be non-member function (ostream is on the left)std::ostream& operator<<(std::ostream& os, const Fixed& fixed) { os << fixed.toFloat(); return os;}
// UsageFixed a(42.42f);std::cout << a << std::endl; // Prints: 42.42196. Fixed-Point Numbers
Section titled “6. Fixed-Point Numbers”What are Fixed-Point Numbers?
Section titled “What are Fixed-Point Numbers?”A way to represent decimal numbers using integers, with a fixed number of bits for the fractional part.
Example: 8 Fractional Bits
Section titled “Example: 8 Fractional Bits”Integer: 42Binary: 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.0fImplementation
Section titled “Implementation”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); }};Why Fixed-Point?
Section titled “Why Fixed-Point?”- Faster than floating-point on some hardware
- Predictable precision
- No floating-point rounding errors for exact values
- Used in: graphics, audio, embedded systems
7. Static Member Functions
Section titled “7. Static Member Functions”min and max Functions
Section titled “min and max Functions”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; }};
// UsageFixed a(2.0f), b(3.0f);std::cout << Fixed::max(a, b) << std::endl; // 3Exercise 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;}Quick Reference
Section titled “Quick Reference”OCF Template
Section titled “OCF Template”class ClassName {public: ClassName(); // Default constructor ClassName(const ClassName& other); // Copy constructor ClassName& operator=(const ClassName& other); // Assignment operator ~ClassName(); // Destructor};Operator Overloading
Section titled “Operator Overloading”| Operator | Member? | Signature |
|---|---|---|
+, -, *, / | Yes | T operator+(const T&) const |
==, !=, <, > | Yes | bool operator==(const T&) const |
++, -- (pre) | Yes | T& operator++() |
++, -- (post) | Yes | T operator++(int) |
<< | No | ostream& operator<<(ostream&, const T&) |
Fixed-Point Conversions
Section titled “Fixed-Point Conversions”// 8 fractional bitsint_to_fixed: value << 8fixed_to_int: value >> 8float_to_fixed: roundf(value * 256)fixed_to_float: value / 256.0fRelated Concepts
Section titled “Related Concepts”Continue your C++ journey:
- Previous: Module 01: Memory & References - Review pointers and references
- Next: Module 03: Inheritance - Learn how to build class hierarchies
Key Terms from This Module
Section titled “Key Terms from This Module”Visit the Glossary for definitions of: