Operator Overloading in C++

What is Operator Overloading?

Operator overloading is a feature in C++ that allows operators such as +, -, *, /, etc., to work with user-defined data types like classes. It enables programmers to define how operators should behave when applied to objects of a class.

Purpose of Operator Overloading

Operator overloading serves several important purposes:

  1. Intuitive Code: Makes code more natural and readable (e.g., using + to add two complex numbers)
  2. Consistency: Allows user-defined types to behave like built-in types
  3. Simplification: Simplifies complex operations into concise notation
  4. Expressiveness: Makes the code more expressive and elegant

Basic Syntax for Operator Overloading

returnType operator symbol (parameters) {
    // Implementation
}

Two Ways to Overload Operators

1. Member Function Method

class ClassName {
public:
    // Operator overloading as a member function
    ReturnType operator symbol (Parameters) {
        // Implementation
    }
};

2. Friend Function Method

class ClassName {
    // Declare friend function for operator overloading
    friend ReturnType operator symbol (Parameters);
};

// Define the operator function outside the class
ReturnType operator symbol (Parameters) {
    // Implementation
}

Example: Overloading the + Operator

Let’s create a simple Complex number class and overload the + operator:

#include <iostream>
using namespace std;

class Complex {
private:
    double real;
    double imag;
    
public:
    // Constructor
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // Method to display complex number
    void display() {
        cout << real;
        if (imag >= 0)
            cout << " + " << imag << "i";
        else
            cout << " - " << -imag << "i";
        cout << endl;
    }
    
    // Overloading + operator as a member function
    Complex operator + (const Complex& obj) {
        Complex temp;
        temp.real = real + obj.real;
        temp.imag = imag + obj.imag;
        return temp;
    }
};

int main() {
    Complex c1(3.0, 4.0);
    Complex c2(1.5, -2.5);
    
    cout << "First complex number: ";
    c1.display();
    
    cout << "Second complex number: ";
    c2.display();
    
    // Using overloaded + operator
    Complex c3 = c1 + c2;
    cout << "Sum of complex numbers: ";
    c3.display();
    
    return 0;
}

Output:

First complex number: 3 + 4i
Second complex number: 1.5 - 2.5i
Sum of complex numbers: 4.5 + 1.5i

Which Operators Can Be Overloaded?

Most operators in C++ can be overloaded, with a few exceptions:

Operators that CAN be overloaded:

  • Arithmetic operators: +, -, *, /, %
  • Relational operators: ==, !=, <, >, <=, >=
  • Logical operators: &&, ||, !
  • Bitwise operators: &, |, ^, ~, <<, >>
  • Assignment operators: =, +=, -=, *=, /=, etc.
  • Increment/decrement: ++, --
  • Memory allocation/deallocation: new, delete
  • Subscript operator: []
  • Function call operator: ()
  • Member selection operators: ->, ->*

Operators that CANNOT be overloaded:

  • Scope resolution operator: ::
  • Member selection operator: .
  • Ternary conditional operator: ?:
  • Size operator: sizeof
  • Type information operator: typeid
  • Member pointer operator: .*

Member Function vs. Friend Function for Operator Overloading

Member Function

  • Has direct access to all members of the class
  • Left operand must be an object of the class
  • Used when operator modifies the left operand
// Overloading + as a member function
Complex operator + (const Complex& obj) {
    Complex temp;
    temp.real = real + obj.real;
    temp.imag = imag + obj.imag;
    return temp;
}

// Usage
Complex c3 = c1 + c2;  // c1 is the calling object

Friend Function

  • Must be declared as a friend within the class
  • Takes all operands as parameters
  • Used when left operand might not be a class object
  • Necessary for overloading input/output operators
// Declaring friend function for + operator
friend Complex operator + (const Complex& left, const Complex& right);

// Defining the friend function
Complex operator + (const Complex& left, const Complex& right) {
    Complex temp;
    temp.real = left.real + right.real;
    temp.imag = left.imag + right.imag;
    return temp;
}

// Usage
Complex c3 = c1 + c2;  // Both c1 and c2 are parameters

Rules for Operator Overloading

  1. Precedence and arity cannot be changed: The precedence (order of operations) and arity (number of operands) remain the same as for built-in types.

  2. Cannot create new operators: You can only overload existing operators.

  3. Default arguments not allowed: Operator functions cannot have default arguments.

  4. Can’t change behavior for built-in types: Operator overloading only affects user-defined types.

  5. Related operators should be consistently overloaded: If you overload ==, you should also overload !=.

Common Operator Overloading Examples

1. Overloading the Stream Insertion (<<) and Extraction (>>) Operators

// Friend functions for input/output operations
friend ostream& operator << (ostream& out, const Complex& c);
friend istream& operator >> (istream& in, Complex& c);

// Definition of stream insertion operator
ostream& operator << (ostream& out, const Complex& c) {
    out << c.real;
    if (c.imag >= 0)
        out << " + " << c.imag << "i";
    else
        out << " - " << -c.imag << "i";
    return out;
}

// Definition of stream extraction operator
istream& operator >> (istream& in, Complex& c) {
    cout << "Enter real part: ";
    in >> c.real;
    cout << "Enter imaginary part: ";
    in >> c.imag;
    return in;
}

// Usage
Complex c1;
cin >> c1;           // Using overloaded >> operator
cout << c1 << endl;  // Using overloaded << operator

2. Overloading Unary Operators (++, —)

// Prefix increment (++obj)
Complex& operator ++ () {
    real++;
    imag++;
    return *this;
}

// Postfix increment (obj++)
// Note the dummy int parameter to distinguish it from prefix
Complex operator ++ (int) {
    Complex temp = *this;  // Save current state
    real++;
    imag++;
    return temp;  // Return saved state
}

// Usage
Complex c1(3.0, 4.0);
++c1;          // Prefix increment
Complex c2 = c1++;  // Postfix increment

3. Overloading Comparison Operators (==, !=)

// Equality operator
bool operator == (const Complex& obj) const {
    return (real == obj.real && imag == obj.imag);
}

// Inequality operator
bool operator != (const Complex& obj) const {
    return !(*this == obj);
}

// Usage
if (c1 == c2) {
    cout << "Complex numbers are equal" << endl;
} else {
    cout << "Complex numbers are not equal" << endl;
}

4. Overloading Assignment Operators (=, +=, -=)

// Assignment operator
Complex& operator = (const Complex& obj) {
    // Check for self-assignment
    if (this != &obj) {
        real = obj.real;
        imag = obj.imag;
    }
    return *this;
}

// Addition assignment operator
Complex& operator += (const Complex& obj) {
    real += obj.real;
    imag += obj.imag;
    return *this;
}

// Usage
c1 = c2;     // Assignment
c1 += c2;    // Addition assignment

5. Overloading Subscript Operator ([])

class Array {
private:
    int* data;
    int size;
    
public:
    // Constructor and other methods...
    
    // Overloading subscript operator
    int& operator [] (int index) {
        if (index < 0 || index >= size) {
            cout << "Array index out of bounds!" << endl;
            exit(1);
        }
        return data[index];
    }
};

// Usage
Array arr(5);
arr[0] = 10;  // Set element
int x = arr[0];  // Get element

Best Practices for Operator Overloading

  1. Maintain natural semantics: Overloaded operators should behave similarly to their built-in counterparts.

  2. Return by value or reference: Return by reference when modifying the object (like +=), return by value when creating a new object (like +).

  3. Consider const correctness: Use const parameters and return const objects when appropriate.

  4. Avoid unexpected side effects: Operators should do what users would naturally expect.

  5. Be consistent: If you overload +, you should probably also overload +=.

  6. Use friend functions appropriately: Use them especially for binary operators where the left operand isn’t your class.

  7. Avoid excessive overloading: Don’t overload operators just because you can; only do it when it makes the code more readable.

Summary

Operator overloading is a powerful feature in C++ that allows custom types to work with standard operators. When used correctly, it makes code more intuitive and readable. By following the rules and best practices, you can create user-defined types that behave naturally and consistently with the rest of the C++ language.

Remember that the goal of operator overloading is to make your code more expressive and easier to understand, not to create confusing or counterintuitive behavior.