3.4 Operator Overloading — Unary, Binary & with Friend Functions
What is Operator Overloading?
Operator overloading lets you redefine operator symbols (+, -, *, ==, etc.) for user-defined types. The same + that adds two ints can be made to "add" two Complex numbers.
Complex c1(3, 4), c2(1, 2);
Complex c3 = c1 + c2; // operator+ is overloaded
This is a form of compile-time polymorphism.
---
Why Overload Operators?
| Benefit | Example |
|---|---|
| Readable code | c1 + c2 vs c1.add(c2) |
| Natural syntax for math types | Complex, Vector, Matrix, BigInt |
| Consistent with built-ins | User-defined types feel "native" |
| Stream support | cout << myObj |
| STL compatibility | Containers, algorithms expect <, ==, etc. |
---
Operators That CAN Be Overloaded
| Category | Operators | ||
|---|---|---|---|
| Arithmetic | + - * / % | ||
| Relational | == != < > <= >= | ||
| Logical | `&& | !` | |
| Bitwise | `& | ^ ~ << >>` | |
| Assignment | `= += -= *= /= %= &= | = ^= <<= >>=` | |
| Increment / Decrement | ++ -- | ||
| Subscript | [] | ||
| Function call | () | ||
| Member access | -> ->* , | ||
| Memory | new new[] delete delete[] | ||
| Stream | << >> (friend functions usually) |
---
Operators That CANNOT Be Overloaded
| Operator | Why |
|---|---|
. (member access) | Reserved |
.* (member-pointer access) | Reserved |
:: (scope resolution) | Compile-time |
?: (ternary) | Conditional, control flow |
sizeof | Compile-time |
typeid | RTTI |
# ## | Preprocessor |
Memorise: "Cannot overload dot, dot-star, scope, ternary, sizeof".
---
Rules of Operator Overloading
- At least one operand must be a user-defined type (can't overload
+for twoints) - Cannot create new operators — only the existing ones
- Cannot change operator precedence or associativity
- Cannot change number of operands (unary stays unary, binary stays binary)
- Some operators must be member functions (
=,[],(),->) - Some operators are typically friend functions (
<<,>>for streams)
---
Two Ways to Overload
1. As a member function
The left operand must be the object on which the operator is called:
class Complex {
private:
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
Complex operator+(const Complex& other) const { // member function
return Complex(real + other.real, imag + other.imag);
}
void display() {
cout << real << " + " << imag << "i" << endl;
}
};
int main() {
Complex c1(3, 4), c2(1, 2);
Complex c3 = c1 + c2; // c1.operator+(c2)
c3.display(); // 4 + 6i
return 0;
}
The expression c1 + c2 is internally interpreted as c1.operator+(c2).
2. As a friend (or non-member) function
If the left operand is not your class — e.g., int + Complex — you need a friend (or non-member) function:
class Complex {
private:
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
friend Complex operator+(const Complex& a, const Complex& b);
friend Complex operator+(double scalar, const Complex& c); // double + Complex
friend Complex operator+(const Complex& c, double scalar); // Complex + double
};
Complex operator+(const Complex& a, const Complex& b) {
return Complex(a.real + b.real, a.imag + b.imag);
}
Complex operator+(double scalar, const Complex& c) {
return Complex(scalar + c.real, c.imag);
}
Complex operator+(const Complex& c, double scalar) {
return Complex(c.real + scalar, c.imag);
}
int main() {
Complex c(3, 4);
Complex r1 = c + 5; // calls Complex+double
Complex r2 = 5 + c; // calls double+Complex
return 0;
}
Best practice: Define arithmetic binary operators as non-member friends for symmetry (handles both sides cleanly). Define modifying operators (+=,-=,=) as members.
---
Unary Operator Overloading
Unary operators act on a single operand: -x, ++x, x++, !x, ~x.
Unary minus
class Vector {
public:
int x, y;
Vector(int a, int b) : x(a), y(b) {}
Vector operator-() const { // unary minus
return Vector(-x, -y);
}
};
Vector v(3, 4);
Vector negV = -v; // (-3, -4)
Prefix vs Postfix ++ / --
class Counter {
private:
int count;
public:
Counter() : count(0) {}
// Prefix ++c
Counter& operator++() {
++count;
return *this;
}
// Postfix c++ — dummy int parameter to differentiate
Counter operator++(int) {
Counter temp = *this; // save current state
++count;
return temp; // return OLD value
}
int getCount() const { return count; }
};
int main() {
Counter c;
++c; // prefix: count = 1, returns ref to current c
c++; // postfix: returns OLD c (count=1), then count becomes 2
cout << c.getCount(); // 2
return 0;
}
Logical NOT !
class Smart {
bool flag;
public:
bool operator!() const { return !flag; }
};
---
Binary Operator Overloading
Binary operators act on two operands: a + b, a == b, a < b.
Arithmetic
Complex Complex::operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
Complex Complex::operator-(const Complex& other) const {
return Complex(real - other.real, imag - other.imag);
}
Complex Complex::operator*(const Complex& other) const {
return Complex(real*other.real - imag*other.imag,
real*other.imag + imag*other.real);
}
Compound assignment
Complex& Complex::operator+=(const Complex& other) {
real += other.real;
imag += other.imag;
return *this;
}
Returns reference to*this— enables chaininga += b += c.
Comparison
bool Complex::operator==(const Complex& other) const {
return real == other.real && imag == other.imag;
}
bool Complex::operator!=(const Complex& other) const {
return !(*this == other);
}
bool Complex::operator<(const Complex& other) const {
return (real*real + imag*imag) < (other.real*other.real + other.imag*other.imag);
}
Stream insertion / extraction (typically friend)
class Complex {
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
friend ostream& operator<<(ostream& os, const Complex& c);
friend istream& operator>>(istream& is, Complex& c);
};
ostream& operator<<(ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os; // return ostream& enables chaining: cout << c1 << c2;
}
istream& operator>>(istream& is, Complex& c) {
cout << "Enter real: ";
is >> c.real;
cout << "Enter imag: ";
is >> c.imag;
return is;
}
int main() {
Complex c(3, 4);
cout << c << endl; // "3 + 4i"
Complex c2(0, 0);
cin >> c2;
cout << "You entered: " << c2 << endl;
return 0;
}
<<and>>must be friend functions (or non-members) because the left operand isostream/istream, not your class.
---
Operator Overloading with Friend Functions
When the left operand is not your class type — like double + Complex — you need a friend or non-member function.
class Complex {
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
friend Complex operator+(double scalar, const Complex& c);
};
Complex operator+(double scalar, const Complex& c) {
return Complex(scalar + c.real, c.imag);
}
Complex r = 5.0 + Complex(2, 3); // 7 + 3i — uses friend
This is why << for ostream is always a friend — the left operand is ostream, which you don't own.
---
Special Operator Overloads
= (Assignment) — must be a member
Complex& Complex::operator=(const Complex& other) {
if (this != &other) {
real = other.real;
imag = other.imag;
}
return *this;
}
= cannot be a friend; the compiler generates a default if you don't.
[] (Subscript) — must be a member
class Array {
int data[100];
public:
int& operator[](int i) {
return data[i]; // returns reference — allows arr[5] = 10;
}
int operator[](int i) const { // const version
return data[i];
}
};
Array a;
a[5] = 10; // OK — uses non-const version
int x = a[5]; // OK — uses const version (if a is const)
() (Function call) — functors
class Adder {
public:
int operator()(int a, int b) { return a + b; }
};
Adder add;
int s = add(3, 5); // 8 — looks like a function call
These are functors — objects that act like functions. Heavily used in STL (std::sort with comparators).
-> (Member access) — for smart pointers
class SmartPtr {
Resource* p;
public:
Resource* operator->() { return p; }
};
SmartPtr sp;
sp->use(); // calls operator->() then ->use()
This is how std::unique_ptr / std::shared_ptr work.
---
Common Operator Overloading Patterns
class Money {
long paise; // 100 paise = ₹1
public:
Money(long p = 0) : paise(p) {}
// Arithmetic
Money operator+(const Money& m) const { return Money(paise + m.paise); }
Money operator-(const Money& m) const { return Money(paise - m.paise); }
Money operator*(int n) const { return Money(paise * n); }
Money& operator+=(const Money& m) { paise += m.paise; return *this; }
// Comparison
bool operator==(const Money& m) const { return paise == m.paise; }
bool operator<(const Money& m) const { return paise < m.paise; }
bool operator>(const Money& m) const { return paise > m.paise; }
// Unary
Money operator-() const { return Money(-paise); }
// Stream
friend ostream& operator<<(ostream& os, const Money& m) {
os << "₹" << (m.paise / 100.0);
return os;
}
};
int main() {
Money a(50000), b(15000); // ₹500 and ₹150
Money sum = a + b; // ₹650
cout << sum << endl; // ₹650
if (a > b) cout << "a is more" << endl;
return 0;
}
---
Member vs Friend / Non-member — exam table
| Operator | Preferred form | Why |
|---|---|---|
= [] () -> | Member | Required by language |
**Arithmetic (+ - /)* | Non-member / friend | Symmetric — handles a + 5 and 5 + a |
Compound assignment (+= -=) | Member | Modifies this |
Comparison (== !=) | Non-member | Symmetric |
<< >> | Friend (non-member) | Left operand is stream |
Unary (-, !, ~) | Member or non-member | Either works |
++ -- | Member | Modifies this |
---
Study deep
- Operator overloading is sugar, not magic.
c1 + c2compiles tooperator+(c1, c2)orc1.operator+(c2). Once you grasp that, the rules become obvious.
- Maintain expected semantics.
+should add (commutative-ish).==should be equivalence.<should be a total order. Surprising users with weird semantics is worse than not overloading at all.
- Provide
==and<for STL. Many STL algorithms need them.std::setneeds<. Containers compare with==. Define them or your class won't work with STL.
- C++20 spaceship operator
<=>. Modern alternative — define one operator and the compiler auto-generates<,<=,>,>=,==,!=:
auto operator<=>(const T& other) const = default;
- Operator overloading is restricted in some languages. Java, Go, Rust limit it (Java forbids; Go forbids; Rust allows via traits). C++ and Python are permissive. The trade-off: power vs potential for abuse.
Key Terms — Lesson 3.4
The terms below cover operator overloading — every PYQ on the topic expects fluent use.
Operator Overloading — Giving custom meanings to existing C++ operators (+, -, <<, ==, [], etc.) when applied to user-defined types. c1 + c2 where c1 and c2 are Complex instances calls a user-defined operator+. A specific case of function overloading.
operator Keyword — The C++ keyword used to declare an overloaded operator. Complex operator+(const Complex& other) const; declares the overloaded + operator as a member. operator+(left, right) declares it as a non-member.
Rules / Restrictions of Operator Overloading — (1) At least one operand must be a user-defined type — you cannot overload operators for two built-in types. (2) Cannot create new operators (@, are illegal). (3) Cannot change operator precedence or associativity. (4) Cannot change the arity (number of operands). (5) Some operators must be member functions** (=, [], (), ->).
Operators That Cannot Be Overloaded — Five operators: :: (scope resolution), . (member access), **. (pointer-to-member access), ?: (ternary), sizeof, and typeid*. These have special compile-time meaning that the language reserves.
Unary Operator Overloading — Overloading operators that take one operand — -x, ++x, x++, !x, ~x, p, &x. As a member, takes no parameter (the operand is this). As a non-member / friend, takes one parameter.
Binary Operator Overloading — Overloading operators that take two operands — a + b, a < b, a == b, a << b. As a member, takes one parameter (the right operand; *this is the left). As a non-member / friend, takes two parameters.
Friend-Based Operator Overloading — Implementing an overloaded operator as a non-member function with friend access. Required for operators where the left operand is not the user's class — most importantly operator<< (left operand is ostream) and arithmetic operators where conversion from a built-in to the class type should work on either side (5 + complex).
Member vs Non-Member Operator — Two ways to implement an overloaded operator. Member functions have implicit this as the left operand; non-members (often friends) take both operands explicitly. Non-members give symmetric behaviour — complex + 5 and 5 + complex both work.
Pre-Increment vs Post-Increment Overloading — Two related but distinct operators. Pre-increment ++x is overloaded as T& operator++(); — returns reference to incremented self. Post-increment x++ is overloaded as T operator++(int); — the dummy int parameter distinguishes it; returns a copy of the original value before increment.
Stream Insertion Operator (<<) Overloading — Custom << for a user-defined class: friend ostream& operator<<(ostream& os, const Complex& c);. Lets cout << complex work. Almost always a friend because the left operand is ostream. Returns the stream by reference to enable chaining (cout << a << b).
Stream Extraction Operator (>>) Overloading — Custom >> for a user-defined class: friend istream& operator>>(istream& is, Complex& c);. Lets cin >> complex work. Same friend reasoning as <<.
Assignment Operator (=) Overloading — Custom assignment: Complex& operator=(const Complex& other);. Must be a member function (cannot be a non-member). The compiler synthesises a member-wise version if you don't write one; classes that manage raw resources should write their own (Rule of Three).
Compound Assignment (+=, -=, etc.) — Operators that combine arithmetic with assignment. Implementing += is often a useful starting point: write += first, then implement + in terms of += for consistency.
Subscript Operator ([]) Overloading — Custom [] to make a class indexable like an array: T& operator[](size_t i);. Must be a member function. Often provide both a non-const and a const version.
Function Call Operator (()) Overloading — Custom () makes an object callable like a function: int operator()(int x) const;. Objects implementing () are called functors / function objects and are the historical predecessor of C++11 lambdas.
Arrow Operator (->) Overloading — Custom -> lets a class behave like a pointer: T operator->();. Used by smart pointers, iterators, optional types — (sptr).method() and sptr->method() both work because of -> overloading.
Functor / Function Object — An object of a class that overloads operator(), making it usable wherever a callable is expected. Functors can carry state (unlike free functions). Pre-C++11, functors were the primary way to write inline callbacks for STL algorithms.
Spaceship Operator (<=>) (C++20) — A modern operator that performs three-way comparison — returns <0, 0, or >0 like strcmp. Define operator<=> once and the compiler auto-generates <, <=, >, >=, ==, != for you. The cleanest way to add full comparison support to a class.
Conversion Operator — A special member function that converts the class type to another type: operator int() const { return value; } lets a Complex behave as an int in some contexts. Powerful but error-prone; modern style marks them explicit to require explicit conversion.
Mutating vs Non-Mutating Operators — Operators like +=, ++ modify the object and typically return T&. Operators like +, -, * do not modify and return a new value. Get this distinction wrong and your overloads will surprise users.
Chaining (Operator Return-Type Convention) — To support a = b = c or cout << a << b, the overloaded operator must return a reference to the appropriate object (typically *this or the left operand). Return-by-value breaks chaining.
Semantics Preservation — The cardinal rule of operator overloading: make the operator do what users intuitively expect. + should add (commutative-ish); == should be equivalence; < should define a total ordering; << should output. Surprising semantics is worse than not overloading at all.
---
PYQ pattern (very common): "What is operator overloading? Overload+operator for a Complex number class." — Define; show Complex class with real + imag; member-function or friend-function form; demonstratec3 = c1 + c2.
PYQ pattern: "Differentiate operator overloading using member function and friend function." — Member:a + bbecomesa.operator+(b); works forComplex + intonly if Complex is left. Friend: works for bothComplex + intandint + Complex. List operators that MUST be members (= [] () ->).
PYQ pattern: "Overload pre-increment and post-increment operators for a Counter class." — Define class; prefix returnsCounter&; postfix takes dummyintand returns Counter by value.
PYQ pattern: "List the rules / restrictions of operator overloading." — 5 rules (at least one user-defined operand; can't create new; can't change precedence; can't change arity; some must be members).