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.
Let's start with a simple example: a 2D Vector class that supports mathematical operations.
// 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
bool operator==(const Vector2D& other) const {
return (x == other.x && y == other.y);
}
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;
}
Not all operators can be overloaded, and there are specific rules about how to implement them correctly.
// 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
}
};
Some operators require special attention and techniques for proper implementation.
// 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> data;
public:
Matrix(size_t rows, size_t cols)
: data(rows, std::vector(cols, 0.0)) {}
// Proxy class for [][] syntax
class RowProxy {
std::vector& row;
public:
RowProxy(std::vector& 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&>(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
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 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(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(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 makes extensive use of operator overloading for its math types.
// 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;
}
};
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?
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 (+=, -=, *=)
Remember: With great power comes great responsibility. Use operator overloading to make code more readable, not to show off clever tricks!