Chapter 12 of 23

Operator Overloading

Learn how to give custom meanings to C++ operators for your own classes — covering member vs friend overloads, arithmetic, comparison, stream, and increment operators with a complete Fraction class example.

Meritshot11 min read
C++Operator OverloadingOOPFriend FunctionsFraction
All C++ Chapters

Operator Overloading in C++

When you write 2 + 3, C++ knows how to add integers. When you write "Hello" + " World" using std::string, C++ also knows how to concatenate — because the + operator was overloaded for std::string by the standard library authors. Operator overloading lets you extend that same expressiveness to your own classes.

For engineering candidates preparing for Wipro Elite, TCS NQT, or FAANG interviews, operator overloading is a reliable topic that appears in both written and practical coding rounds.


Which Operators Can Be Overloaded?

Most C++ operators can be overloaded. A handful cannot.

Can OverloadCannot Overload
+ - * / %:: (scope resolution)
== != < > <= >=. (member access)
<< >> (stream).* (pointer-to-member)
++ -- (pre and post)?: (ternary)
[] () ->sizeof
= += -= *= /=typeid
new deletestatic_cast and related casts

Key rule: Operator overloading can change what an operator does for your type, but it cannot change the operator's arity (number of operands), precedence, or associativity.


Member Function vs Friend Function

This is the most common conceptual question in C++ interviews. You have two ways to overload an operator.

As a Member Function

The left-hand operand is always the calling object (*this). This is natural for operators like +=, [], and ().

class Vector2D {
public:
    double x, y;
    Vector2D(double x, double y) : x(x), y(y) {}

    // Member function: left operand is *this
    Vector2D operator+(const Vector2D& other) const {
        return Vector2D(x + other.x, y + other.y);
    }
};

int main() {
    Vector2D a(1.0, 2.0), b(3.0, 4.0);
    Vector2D c = a + b;   // Calls a.operator+(b)
    return 0;
}

As a Friend Function

Friend functions have access to private members but are not member functions themselves. They are preferred for operators where the left operand might be a built-in type (like int or ostream), because built-in types cannot have member functions.

class Vector2D {
public:
    double x, y;
    Vector2D(double x, double y) : x(x), y(y) {}

    // Friend declaration
    friend ostream& operator<<(ostream& os, const Vector2D& v);
};

// Definition outside the class
ostream& operator<<(ostream& os, const Vector2D& v) {
    os << "(" << v.x << ", " << v.y << ")";
    return os;
}

int main() {
    Vector2D a(1.0, 2.0);
    cout << a << endl;   // Calls operator<<(cout, a)
    return 0;
}

You cannot make operator<< a member of Vector2D because the left operand (cout) is of type ostream, not Vector2D.


Comparison Guide: Member vs Friend

ScenarioPreferred Approach
Left operand is always *thisMember function
Left operand is a built-in or standard typeFriend function
Symmetric binary operators (+, ==)Friend function (more symmetric)
Assignment-like operators (=, +=)Member function (required for =)
Stream operators << and >>Friend function (required)

Overloading Arithmetic Operators

#include <iostream>
using namespace std;

class Complex {
    double real, imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // Unary minus
    Complex operator-() const {
        return Complex(-real, -imag);
    }

    // Binary plus as friend
    friend Complex operator+(const Complex& a, const Complex& b);
    friend Complex operator-(const Complex& a, const Complex& b);
    friend Complex operator*(const Complex& a, const Complex& b);

    void print() const {
        cout << real << " + " << imag << "i" << endl;
    }
};

Complex operator+(const Complex& a, const Complex& b) {
    return Complex(a.real + b.real, a.imag + b.imag);
}

Complex operator-(const Complex& a, const Complex& b) {
    return Complex(a.real - b.real, a.imag - b.imag);
}

Complex operator*(const Complex& a, const Complex& b) {
    return Complex(
        a.real * b.real - a.imag * b.imag,
        a.real * b.imag + a.imag * b.real
    );
}

int main() {
    Complex c1(3.0, 4.0), c2(1.0, 2.0);
    (c1 + c2).print();   // 4 + 6i
    (c1 - c2).print();   // 2 + 2i
    (c1 * c2).print();   // -5 + 10i
    return 0;
}

Overloading Comparison Operators

class Student {
    string name;
    double cgpa;
public:
    Student(string n, double g) : name(n), cgpa(g) {}

    bool operator==(const Student& other) const {
        return name == other.name && cgpa == other.cgpa;
    }

    bool operator!=(const Student& other) const {
        return !(*this == other);
    }

    bool operator<(const Student& other) const {
        return cgpa < other.cgpa;
    }

    bool operator>(const Student& other) const {
        return other < *this;
    }

    // Useful when storing in std::sort or std::set
    bool operator<=(const Student& other) const {
        return !(other < *this);
    }

    bool operator>=(const Student& other) const {
        return !(*this < other);
    }
};

Notice the pattern: once you define < and ==, you can express all others in terms of those two. This avoids code duplication.


Overloading Stream Operators

Stream operators are the most practically useful overloads — they let your class work naturally with cout, cin, and file streams.

#include <iostream>
#include <string>
using namespace std;

class Employee {
    string name;
    int id;
    double salary;  // in lakhs per annum
public:
    Employee() : name(""), id(0), salary(0) {}
    Employee(string n, int i, double s) : name(n), id(i), salary(s) {}

    // Output stream operator
    friend ostream& operator<<(ostream& os, const Employee& e) {
        os << "ID: " << e.id
           << " | Name: " << e.name
           << " | Salary: Rs." << e.salary << " LPA";
        return os;
    }

    // Input stream operator
    friend istream& operator>>(istream& is, Employee& e) {
        cout << "Enter name, ID, salary (LPA): ";
        is >> e.name >> e.id >> e.salary;
        return is;
    }
};

int main() {
    Employee e1("Rohan", 1001, 12.5);
    cout << e1 << endl;

    Employee e2;
    cin >> e2;
    cout << e2 << endl;
    return 0;
}

Both operators return a reference to the stream, which is what allows chaining like cout << a << b << endl.


Overloading Pre and Post Increment

The pre-increment and post-increment operators have the same name (++) but different signatures. C++ distinguishes them with a dummy int parameter for the post-increment version.

class Counter {
    int value;
public:
    Counter(int v = 0) : value(v) {}

    // Pre-increment: ++c
    Counter& operator++() {
        ++value;
        return *this;   // Return reference to modified object
    }

    // Post-increment: c++
    // The dummy int parameter signals this is post-increment
    Counter operator++(int) {
        Counter old = *this;   // Save old state
        ++value;
        return old;            // Return copy of old state
    }

    int get() const { return value; }
};

int main() {
    Counter c(5);

    Counter a = ++c;   // c becomes 6, a gets 6
    cout << "Pre:  a=" << a.get() << " c=" << c.get() << endl;

    Counter b = c++;   // b gets 6, c becomes 7
    cout << "Post: b=" << b.get() << " c=" << c.get() << endl;
    return 0;
}

Output:

Pre:  a=6 c=6
Post: b=6 c=7

Pre-increment is more efficient because it avoids creating a temporary copy. Prefer ++i over i++ in loops.


Worked Example: The Fraction Class

Let us build a complete Fraction class that supports all arithmetic operators, comparisons, and stream I/O. This is a classic C++ interview problem given at companies like Adobe India and Amazon Bangalore.

#include <iostream>
#include <stdexcept>
#include <numeric>   // for gcd (C++17) or __gcd
using namespace std;

class Fraction {
    long long num, den;   // numerator, denominator

    void simplify() {
        if (den < 0) { num = -num; den = -den; }
        long long g = __gcd(abs(num), abs(den));
        num /= g;
        den /= g;
    }

public:
    Fraction(long long n = 0, long long d = 1) : num(n), den(d) {
        if (d == 0) throw invalid_argument("Denominator cannot be zero");
        simplify();
    }

    // --- Arithmetic ---

    Fraction operator+(const Fraction& other) const {
        return Fraction(num * other.den + other.num * den, den * other.den);
    }

    Fraction operator-(const Fraction& other) const {
        return Fraction(num * other.den - other.num * den, den * other.den);
    }

    Fraction operator*(const Fraction& other) const {
        return Fraction(num * other.num, den * other.den);
    }

    Fraction operator/(const Fraction& other) const {
        return Fraction(num * other.den, den * other.num);
    }

    // Unary minus
    Fraction operator-() const {
        return Fraction(-num, den);
    }

    // Compound assignment
    Fraction& operator+=(const Fraction& other) {
        *this = *this + other;
        return *this;
    }

    // --- Comparison ---

    bool operator==(const Fraction& other) const {
        return num == other.num && den == other.den;
    }

    bool operator!=(const Fraction& other) const { return !(*this == other); }

    bool operator<(const Fraction& other) const {
        return num * other.den < other.num * den;
    }

    bool operator>(const Fraction& other) const  { return other < *this; }
    bool operator<=(const Fraction& other) const { return !(*this > other); }
    bool operator>=(const Fraction& other) const { return !(*this < other); }

    // --- Stream Operators ---

    friend ostream& operator<<(ostream& os, const Fraction& f) {
        if (f.den == 1) os << f.num;
        else            os << f.num << "/" << f.den;
        return os;
    }

    friend istream& operator>>(istream& is, Fraction& f) {
        char slash;
        is >> f.num >> slash >> f.den;
        f.simplify();
        return is;
    }

    // Convert to double for display
    double toDouble() const { return static_cast<double>(num) / den; }
};

int main() {
    Fraction a(1, 2);   // 1/2
    Fraction b(1, 3);   // 1/3
    Fraction c(2, 4);   // Simplifies to 1/2

    cout << a << " + " << b << " = " << (a + b) << endl;
    cout << a << " - " << b << " = " << (a - b) << endl;
    cout << a << " * " << b << " = " << (a * b) << endl;
    cout << a << " / " << b << " = " << (a / b) << endl;

    cout << "\na == c? " << (a == c ? "true" : "false") << endl;
    cout << "a < b?  " << (a < b  ? "true" : "false") << endl;

    Fraction sum(0, 1);
    sum += a;
    sum += b;
    cout << "\nRunning sum: " << sum << " = " << sum.toDouble() << endl;

    return 0;
}

Output:

1/2 + 1/3 = 5/6
1/2 - 1/3 = 1/6
1/2 * 1/3 = 1/6
1/2 / 1/3 = 3/2

a == c? true
a < b?  false

Running sum: 5/6 = 0.833333

The simplify() method inside the constructor ensures all fractions are stored in their lowest terms, so 2/4 and 1/2 compare as equal.


Common Pitfalls

  1. Forgetting to return *this from compound assignments. Operators like +=, -=, and = must return a reference to *this to allow chaining: a += b += c.

  2. Post-increment returning a reference. Post-increment must return a copy (value), not a reference, because it returns the old value before the increment. Returning a reference to a local variable is undefined behaviour.

  3. Not handling self-assignment in operator=. Always guard against a = a with if (this == &other) return *this; before doing any cleanup.

  4. Forgetting const on the right-hand operand. Binary operators like + and == should take the right-hand operand as const reference. Without const, you cannot call them on const objects.

  5. Overloading && and ||. Overloading these breaks short-circuit evaluation — both operands are always evaluated. Avoid overloading logical operators.

  6. Ambiguous conversions. If you provide implicit conversion constructors alongside overloaded operators, the compiler may generate multiple valid interpretations and refuse to compile. Prefer explicit constructors.


Practice Exercises

  1. Add a % (modulo) operator to the Fraction class that computes the fractional remainder of dividing one fraction by another.

  2. Build a Matrix2x2 class that overloads +, -, * (matrix multiplication), ==, and <<. Test it with rotation matrices used in 2D graphics.

  3. Write a BigInteger class that overloads + for adding integers of arbitrary length stored as strings (a common problem in competitive programming contests on platforms like Codeforces).

  4. Implement a Date class with ++ (advance by one day, handling month and year rollovers), --, - (difference in days between two dates), and << for printing in DD/MM/YYYY format.

  5. Extend the Employee class from earlier with < and > operators that compare by salary. Use these with std::sort to sort a vector of employees from highest to lowest salary — practical for payroll systems at firms like TCS HR software.


Summary

  • Operator overloading lets you give user-defined types natural mathematical and I/O syntax.
  • Most operators can be overloaded; ::, ., .*, ?:, sizeof, and casts cannot.
  • Use member functions when the left operand is always *this; use friend functions for symmetric binary operators and stream operators.
  • operator<< and operator>> must be friend (or non-member) functions because the left operand is ostream or istream.
  • Pre-increment returns *this by reference; post-increment saves a copy, increments, and returns the copy by value.
  • Always keep operator!= consistent with ==, and >, <=, >= consistent with <.
  • The Fraction class demonstrates complete arithmetic, comparison, and stream operator support, simplified via GCD on construction.