📝 Lesson 13: Operator Overloading
Make your custom types work naturally with operators like +, -, ==, and the stream operators << and >>.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Understand the purpose and syntax of operator overloading in C++.
- Implement both member and non-member operator functions appropriately.
- Follow C++ conventions for return types and const-correctness.
- Handle assignment and compound assignment operators correctly (including self-assignment).
- Implement type conversion operators and understand when implicit vs. explicit conversion is appropriate.
Estimated Time: 60–75 minutes
Project: Implement a Matrix2x2 class with support for arithmetic, comparison, and stream operators.
In This Lesson
What is Operator Overloading?
Operator overloading is like teaching your custom types to speak the language of mathematics and logic. Instead of calling functions like add() or equals(), you can use natural operators like + and ==, making your code more intuitive and readable.
Basic Operator Overloading
Let's start with a simple example: a 2D Vector class that supports mathematical operations.
Vector2D Example
// Basic 2D Vector class with operator overloading
class Vector2D {
private:
float x, y;
public:
// Constructor
Vector2D(float x = 0.0f, float y = 0.0f) : x(x), y(y) {}
// Getters
float GetX() const { return x; }
float GetY() const { return y; }
// Addition operator (member function)
Vector2D operator+(const Vector2D& other) const {
return Vector2D(x + other.x, y + other.y);
}
// Subtraction operator
Vector2D operator-(const Vector2D& other) const {
return Vector2D(x - other.x, y - other.y);
}
// Scalar multiplication (member function)
Vector2D operator*(float scalar) const {
return Vector2D(x * scalar, y * scalar);
}
// Compound assignment operators
Vector2D& operator+=(const Vector2D& other) {
x += other.x;
y += other.y;
return *this; // Return reference for chaining
}
Vector2D& operator-=(const Vector2D& other) {
x -= other.x;
y -= other.y;
return *this;
}
// Unary minus (negation)
Vector2D operator-() const {
return Vector2D(-x, -y);
}
// Equality comparison.
// Since C++20 you can replace both of these with a single line:
// bool operator==(const Vector2D&) const = default;
// The compiler then generates a correct member-wise == for you, and
// synthesizes != from it automatically. See the C++20 section below.
[[nodiscard]] bool operator==(const Vector2D& other) const {
return (x == other.x && y == other.y);
}
// Pre-C++20 you had to supply != by hand (C++20 derives it from ==):
[[nodiscard]] bool operator!=(const Vector2D& other) const {
return !(*this == other); // Reuse == operator
}
// Subscript operator for array-like access
float operator[](int index) const {
if (index == 0) return x;
if (index == 1) return y;
throw std::out_of_range("Vector2D index out of range");
}
float& operator[](int index) {
if (index == 0) return x;
if (index == 1) return y;
throw std::out_of_range("Vector2D index out of range");
}
};
// Non-member operator for scalar * vector
Vector2D operator*(float scalar, const Vector2D& vec) {
return vec * scalar; // Reuse member operator
}
// Stream operators (non-member)
std::ostream& operator<<(std::ostream& os, const Vector2D& vec) {
os << "(" << vec.GetX() << ", " << vec.GetY() << ")";
return os;
}
std::istream& operator>>(std::istream& is, Vector2D& vec) {
float x, y;
char c1, c2, c3; // For parentheses and comma
is >> c1 >> x >> c2 >> y >> c3;
if (c1 == '(' && c2 == ',' && c3 == ')') {
vec = Vector2D(x, y);
}
return is;
}
Operator Overloading Rules
Not all operators can be overloaded, and there are specific rules about how to implement them correctly.
Implementation Guidelines
// 1. Member vs Non-Member Functions
class Complex {
double real, imag;
public:
// Member function - when left operand is always this type
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// Friend function - needs access to private members
friend Complex operator*(double scalar, const Complex& c);
};
// Non-member - allows scalar * complex
Complex operator*(double scalar, const Complex& c) {
return Complex(scalar * c.real, scalar * c.imag);
}
// 2. Return Types Convention
class String {
public:
// Value return for arithmetic
String operator+(const String& other) const {
String result = *this;
result.append(other);
return result;
}
// Reference return for assignment
String& operator=(const String& other) {
if (this != &other) { // Check self-assignment
// Copy data
}
return *this; // Enable chaining: a = b = c
}
// Reference return for compound assignment
String& operator+=(const String& other) {
append(other);
return *this;
}
// Bool return for comparison
bool operator==(const String& other) const {
return compare(other) == 0;
}
// Reference return for subscript
char& operator[](size_t index) {
return data[index];
}
// Const version for const objects
const char& operator[](size_t index) const {
return data[index];
}
};
// 3. Pre vs Post Increment/Decrement
class Counter {
int value;
public:
// Pre-increment (++counter) - more efficient
Counter& operator++() {
++value;
return *this;
}
// Post-increment (counter++) - note dummy int parameter
Counter operator++(int) {
Counter temp = *this; // Save old value
++value;
return temp; // Return old value
}
};
C++20: Defaulted Comparisons and the Spaceship Operator
Before C++20, a type that needed the full family of relational operators had to hand-write all six of them (==, !=, <, >, <=, >=) — tedious and easy to get inconsistent. C++20 lets the compiler generate them from a single three-way comparison operator, operator<=> (nicknamed the "spaceship" operator), plus a defaulted operator==.
#include <compare> // std::strong_ordering, std::partial_ordering, ...
struct Version {
int major, minor, patch;
// One line generates <, >, <=, >= (member-wise, in declaration order)...
auto operator<=>(const Version&) const = default;
// ...and a defaulted == (which also gives you != for free).
bool operator==(const Version&) const = default;
};
Version a{1, 2, 0}, b{1, 3, 0};
bool older = a < b; // true — synthesized from <=>
bool same = a == b; // false — from defaulted ==
auto ordering = a <=> b; // std::strong_ordering::less
The result type of <=> describes how strong the ordering is:
std::strong_ordering— a total order where equal values are truly interchangeable (integers, enums).std::weak_ordering— a total order where "equivalent" values may still differ (e.g. case-insensitive strings).std::partial_ordering— some pairs are unordered. Floating-point types use this becauseNaNcompares unordered against everything.
That last point matters for the earlier Vector2D: because its members are float, a defaulted operator<=> would yield std::partial_ordering. Defaulted comparisons are implicitly constexpr and noexcept when their members allow it, so you get compile-time-friendly, exception-safe comparisons at no extra cost. When you need custom logic (say, comparing a fraction by value rather than field-by-field), write your own operator<=> and pick the return category yourself.
Advanced Operator Overloading
Some operators require special attention and techniques for proper implementation.
Function Call and Subscript Operators
// Function object (Functor)
class LinearFunction {
double a, b; // f(x) = ax + b
public:
LinearFunction(double a, double b) : a(a), b(b) {}
// Function call operator
double operator()(double x) const {
return a * x + b;
}
// Multiple parameters
double operator()(double x, double y) const {
return a * x + b * y;
}
};
// Matrix class with 2D subscript
class Matrix {
std::vector<std::vector<double>> data;
public:
Matrix(size_t rows, size_t cols)
: data(rows, std::vector<double>(cols, 0.0)) {}
// Proxy class for [][] syntax
class RowProxy {
std::vector<double>& row;
public:
RowProxy(std::vector<double>& r) : row(r) {}
double& operator[](size_t col) { return row[col]; }
const double& operator[](size_t col) const { return row[col]; }
};
// First [] returns proxy
RowProxy operator[](size_t row) {
return RowProxy(data[row]);
}
const RowProxy operator[](size_t row) const {
return RowProxy(const_cast<std::vector<double>&>(data[row]));
}
// Alternative: operator() for 2D access
double& operator()(size_t row, size_t col) {
return data[row][col];
}
const double& operator()(size_t row, size_t col) const {
return data[row][col];
}
};
// Smart pointer operator->
template<typename T>
class SmartPtr {
T* ptr;
public:
explicit SmartPtr(T* p = nullptr) : ptr(p) {}
~SmartPtr() { delete ptr; }
// Dereference operators
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
// Boolean conversion
explicit operator bool() const { return ptr != nullptr; }
};
Type Conversion Operators
Type conversion operators allow your objects to be converted to other types implicitly or explicitly.
// Type conversion operators
class Fraction {
int numerator, denominator;
public:
Fraction(int n = 0, int d = 1) : numerator(n), denominator(d) {}
// Implicit conversion to double (be careful!)
operator double() const {
return static_cast<double>(numerator) / denominator;
}
// Explicit conversion to int (C++11)
explicit operator int() const {
return numerator / denominator;
}
// Conversion to bool for if statements
explicit operator bool() const {
return numerator != 0;
}
};
// Usage
Fraction f(3, 4);
double d = f; // OK: implicit conversion to double (0.75)
// int i = f; // ERROR: explicit conversion required
int i = static_cast<int>(f); // OK: explicit conversion (0)
if (f) { } // OK: explicit bool conversion
// Safe bool idiom (pre-C++11)
class OldStyleClass {
typedef void (OldStyleClass::*bool_type)() const;
void safe_bool_helper() const {}
public:
operator bool_type() const {
return isValid() ? &OldStyleClass::safe_bool_helper : nullptr;
}
};
Unreal Engine Operator Examples
Unreal Engine makes extensive use of operator overloading for its math types.
Unreal Engine Examples
// Unreal Engine operator overloading examples
// FVector operators
void VectorOperations() {
FVector A(1.0f, 2.0f, 3.0f);
FVector B(4.0f, 5.0f, 6.0f);
// Addition and subtraction
FVector Sum = A + B;
FVector Diff = A - B;
// Scalar multiplication
FVector Scaled = A * 2.0f;
FVector ScaledLeft = 2.0f * A; // Both work
// Dot product (| operator)
float DotProduct = A | B;
// Cross product (^ operator)
FVector CrossProduct = A ^ B;
// Component-wise multiplication
FVector ComponentMult = A * B;
// Comparison
if (A == B) { } // Exact equality (rare)
if (A.Equals(B, 0.01f)) { } // With tolerance
}
// FRotator operators
void RotatorOperations() {
FRotator Rot1(30.0f, 45.0f, 0.0f); // Pitch, Yaw, Roll
FRotator Rot2(10.0f, -15.0f, 0.0f);
// Combine rotations
FRotator Combined = Rot1 + Rot2;
// Difference
FRotator Delta = Rot1 - Rot2;
// Scaling rotation
FRotator HalfRot = Rot1 * 0.5f;
}
// FTransform operators
void TransformOperations() {
FTransform T1(FRotator(0, 90, 0), FVector(100, 0, 0));
FTransform T2(FRotator(0, 45, 0), FVector(50, 50, 0));
// Combine transforms (applies T2 then T1)
FTransform Combined = T1 * T2;
// Transform a point
FVector Point(10, 20, 30);
FVector TransformedPoint = T1.TransformPosition(Point);
}
// Custom Unreal types with operators
USTRUCT()
struct FDamageInfo {
GENERATED_BODY()
UPROPERTY()
float BaseDamage = 0.0f;
UPROPERTY()
float Multiplier = 1.0f;
// Addition operator for combining damage
FDamageInfo operator+(const FDamageInfo& Other) const {
FDamageInfo Result;
Result.BaseDamage = BaseDamage + Other.BaseDamage;
Result.Multiplier = Multiplier * Other.Multiplier;
return Result;
}
// Scalar multiplication for damage scaling
FDamageInfo operator*(float Scale) const {
FDamageInfo Result = *this;
Result.BaseDamage *= Scale;
return Result;
}
// Get final damage
float GetTotalDamage() const {
return BaseDamage * Multiplier;
}
};
Common Pitfalls and Solutions
Avoid these common mistakes when overloading operators.
// Pitfall 1: Inconsistent behavior
class Bad1 {
int value;
public:
// BAD: + doesn't return sum!
Bad1 operator+(const Bad1& other) {
std::cout << "Adding\n"; // Side effect!
return *this; // Returns first operand, not sum
}
};
// Solution: Follow expected behavior
class Good1 {
int value;
public:
Good1 operator+(const Good1& other) const {
return Good1(value + other.value); // Returns actual sum
}
};
// Pitfall 2: Not handling self-assignment
class Bad2 {
int* data;
public:
Bad2& operator=(const Bad2& other) {
delete data; // Problem if this == &other!
data = new int(*other.data);
return *this;
}
};
// Solution: Check for self-assignment
class Good2 {
int* data;
public:
Good2& operator=(const Good2& other) {
if (this != &other) { // Self-assignment check
delete data;
data = new int(*other.data);
}
return *this;
}
};
// Pitfall 3: Wrong return types
class Bad3 {
public:
// BAD: Assignment should return reference
Bad3 operator=(const Bad3& other) { // Returns by value!
// ...
return *this;
}
// BAD: Comparison should return bool
int operator==(const Bad3& other) { // Returns int!
return 1;
}
};
// Pitfall 4: Asymmetric operators
class Temperature {
double celsius;
public:
// Member function only allows Temperature + double
Temperature operator+(double degrees) const {
return Temperature(celsius + degrees);
}
};
// Solution: Use non-member functions for symmetry
Temperature operator+(double degrees, const Temperature& temp) {
return temp + degrees; // Reuse member operator
}
// Pitfall 5: Implicit conversions causing ambiguity
class Number {
int value;
public:
Number(int v) : value(v) {} // Implicit conversion from int
operator int() const { return value; } // Implicit conversion to int
Number operator+(const Number& other) const {
return Number(value + other.value);
}
};
// Usage causes ambiguity:
// Number n1(5), n2(10);
// Number n3 = n1 + 5; // Ambiguous! int->Number or Number->int?
Practice Exercise: Matrix Class
🏋️ Challenge: Implement a 2x2 Matrix Class
Create a Matrix2x2 class with proper operator overloading:
// Implement this Matrix2x2 class
class Matrix2x2 {
private:
float m[2][2];
public:
// TODO: Implement these operators
// 1. Constructor that takes 4 floats
// 2. Addition: Matrix + Matrix
// 3. Subtraction: Matrix - Matrix
// 4. Multiplication: Matrix * Matrix
// 5. Scalar multiplication: Matrix * float and float * Matrix
// 6. Equality: Matrix == Matrix
// 7. Subscript: Matrix[row][col] for access
// 8. Stream output: cout << Matrix
// 9. Determinant function
// 10. Inverse operator: ~Matrix (returns inverse)
// Test your implementation:
// Matrix2x2 A(1, 2, 3, 4);
// Matrix2x2 B(5, 6, 7, 8);
// Matrix2x2 C = A + B;
// Matrix2x2 D = A * B;
// float det = A.Determinant();
// Matrix2x2 Inv = ~A;
// cout << "Matrix A:\n" << A << endl;
};
// Bonus: Add these features
// - Transpose operator: !Matrix
// - Identity matrix static function
// - Rotation matrix factory function
// - Compound assignment operators (+=, -=, *=)
🎯 Quick Quiz
Question 1: Why should operator= check for self-assignment with something like if (this != &other)?
Question 2: What is the conventional return type for a compound assignment operator such as operator+=?
Question 3: In the lesson's Fraction and SmartPtr examples, what does marking a conversion operator explicit — as in explicit operator bool() const — actually do?
Summary
🎉 Key Takeaways
- ✓ Operator overloading makes custom types behave like built-in types
- ✓ Keep the natural meaning of operators - don't surprise users
- ✓ Use member functions when the left operand is always your type
- ✓ Use non-member functions for symmetric operations
- ✓ Return appropriate types: references for assignment, values for arithmetic
- ✓ Always handle self-assignment in operator=
- ✓ In C++20, prefer a defaulted
operator<=>/operator==over hand-writing all six comparisons - ✓ Make operators const-correct
- ✓ Consider making single-parameter constructors explicit
- ✓ Be careful with implicit conversions
- ✓ Some operators cannot be overloaded (::, ., .*, ?:)
Remember: With great power comes great responsibility. Use operator overloading to make code more readable, not to show off clever tricks!
📚 Additional Resources
- cppreference — Operator overloading
- cppreference — User-defined conversion functions
- isocpp.org — Operator Overloading FAQ
🚀 What's Next?
You now know how to make custom C++ types read and write naturally with the language's own operators. Next, in Lesson 14: Header Files and Forward Declarations, you'll organize all these classes across multiple files — using headers and forward declarations to keep large projects compiling cleanly and quickly.
🎉 You finished the course!
From your first "Hello, World!" to overloading operators on your own types — that's a real journey, and you walked every step of it. Go build something great with it.