Categories
Computer Science / Information Technology Language: C++

Operator overloading

Overview

Operator overloading is a feature in C++ that allows operators to be given new meanings when applied to user-defined data types. This means that operators such as +, -, *, /, etc. can be used with objects of a class in the same way as they are used with built-in data types. This can make code more readable and writable. These are not implicit, i.e., not done automatically (except for the assignment operator), they must be explicitly defined.

Operator overloading in perspective of using objects

Let’s consider an example of a class Point that represents a point in a two-dimensional plane with coordinates x and y. We want to be able to add two Point objects using the + operator.

Without operator overloading, we would need to define a member function to add two Point objects, such as:

class Point {
public:
    Point(int x, int y) : x(x), y(y) {}
    Point add(const Point& other) const {
        return Point(x + other.x, y + other.y);
    }
private:
    int x, y;
};

To add two Point objects, we would have to call the add member function explicitly:

Point p1(1, 2);
Point p2(3, 4);
Point p3 = p1.add(p2);

Or worse off, there could be a static method or a function called add() that takes two parameters: objects of Point class that need to be added. we would have to call the add member function explicitly:

Point p3 = add(p1, p2);

With operator overloading, we can define the + operator to perform the addition of two Point objects:

Point p1(1, 2);
Point p2(3, 4);
Point p3 = p1 + p2;

As we can see, operator overloading provides a more natural and intuitive way of expressing operations on objects. It allows us to use familiar syntax for built-in types with user-defined types, making our code more readable and easier to understand. In this section, we’ll extensively dive into details of operator overloading.

Operators that support overloading

In C++, the following operators can be overloaded using operator overloading:

  1. Arithmetic operators: +, -, *, /, %, +=, -=, *=, /=, %=
  2. Comparison operators: ==, !=, <, >, <=, >=
  3. Logical operators: !, &&, ||
  4. Bitwise operators: &, |, ^, ~, <<, >>
  5. Assignment operators: =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
  6. Increment and decrement operators: ++, --
  7. Function call operator: ()
  8. Subscript operator: []
  9. Member selection operator: ->
  10. Comma operator: ,

Also, in C++, not all operators can be overloaded. The following operators cannot be overloaded:

  1. :: – Scope resolution operator
  2. . – Member selection operator
  3. .* – Pointer-to-member selection operator
  4. sizeof – Size-of operator
  5. typeid – Type information operator
  6. ?: – Ternary conditional operator (conditional operator can be overloaded, but ?: specifically cannot)
  7. # – Preprocessor operator
  8. ## – Token pasting operator (used in macro definitions)

These operators are either completely built into the language (e.g., sizeof, typeid) or have a specific syntax that cannot be changed (e.g., . for member selection and :: for scope resolution).

Basic rules of operator overloading

The basic rules of operator overloading in C++ are:

  1. Only existing operators can be overloaded, and new operators cannot be created.
  2. The number of operands (‘arity’) cannot be changed. For example, the addition operator + always takes two operands, and this cannot be changed.
  3. Precedence and associativity cannot be changed.
  4. Overloaded operators must obey the same syntax and precedence rules as the built-in operators.
  5. Overloaded operators should not modify their operands, except when it makes sense to do so, like in the case of the assignment operator =.
  6. Some operators, such as the conditional operator ?:, require multiple overloaded versions to cover all possible operand types.
  7. [], (), ->, and the assignment operator (=) must be declared as member method while other operators can be declared as member methods or global functions.

By following these rules, C++ allows programmers to redefine the behaviour of built-in operators to work with user-defined types.

Syntax of operator overloading

The syntax of operator overloading in C++ is as follows:

ReturnType operator Symbol (ParameterList) {
    // code to perform the operation
    return Result;
}

Here, ReturnType is the return type of the overloaded operator, Symbol is the symbol of the operator being overloaded (such as +, -, *, /, ==, etc.), ParameterList is the list of parameters that the operator takes, and the body of the function contains the code that performs the operation.

Overloading Arithmetic operators

Overloading the addition operator (+)

Overloading the addition operator (+) in a class allows objects of that class to be added using the + operator. This is useful when the addition of objects of a certain class has a specific meaning or operation.

The syntax for overloading the addition operator in a class is as follows:

ReturnType operator+(const ClassName& obj) const;

Here,

  • ReturnType is the return type of the overloaded operator.
  • operator+ is the addition operator.
  • ClassName is the name of the class.
  • obj is the object to be added to the current object.
  • The const keyword is used to indicate that the operator function does not modify the state of the object.

Let’s consider an example of a class Complex that represents complex numbers:

class Complex {
    private:
        double real;
        double imag;
    public:
        Complex(double r = 0, double i = 0) : real(r), imag(i) {}
        Complex operator+(const Complex& obj) const {
            Complex res;
            res.real = real + obj.real;
            res.imag = imag + obj.imag;
            return res;
        }
        void display() {
            cout << real << " + " << imag << "i" << endl;
        }
};

In this example, we have overloaded the addition operator using the operator+ function. This function takes an object of the class Complex as an argument and returns an object of the same class. The function creates a new object res, sets its real and imag members to the sum of the corresponding members of the two input objects, and returns the new object. The display function is used to print the complex number in the format real + imagi.

Now, we can use the addition operator to add two Complex objects as follows:

Complex c1(2, 3);
Complex c2(4, 5);
Complex c3 = c1 + c2;
c3.display();   // Output: 6 + 8i

In this example, we have created two Complex objects c1 and c2 and added them using the + operator. The result is stored in a new Complex object c3, which is then displayed.

Overloading subtraction operator

Let’s consider the same example of the Complex class used earlier. To overload the subtraction operator - for this class, we can define a member function named operator- that takes another Complex object as an argument and returns a Complex object as the result of the subtraction operation.

Here’s an example implementation of the subtraction overloading method:

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

In this implementation, the operator- function takes a const Complex& parameter named other, which represents the Complex object to be subtracted from the current object. The function returns a new Complex object that represents the result of the subtraction.

For example, if we have two Complex objects c1 and c2, we can subtract c2 from c1 using the overloaded - operator as follows:

Complex c1(2.0, 3.0);
Complex c2(1.0, 2.0);
Complex result = c1 - c2;  // calls operator- function

In this case, the result object will have a real part of 1.0 and an imaginary part of 1.0, which is the result of subtracting c2 from c1.

Note that we can also overload the - operator as a non-member function or a friend function of the Complex class, just like the + operator. The syntax for this would be:

Complex operator-(const Complex& c1, const Complex& c2) {
    return Complex(c1.real - c2.real, c1.imag - c2.imag);
}

This function can be used in the same way as the member function overload.

Overloading other arithmetic operators

Similar to the examples seen above for addition and subtraction operator overloading, other arithmetic operators such as multiplication, division and modulus operators can be overloading. The following code does the same:

#include <iostream>

class Complex {
private:
    double real;
    double imag;

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

    // Overload * operator
    Complex operator*(const Complex& c) const {
        double r = real * c.real - imag * c.imag;
        double i = real * c.imag + imag * c.real;
        return Complex(r, i);
    }

    // Overload / operator
    Complex operator/(const Complex& c) const {
        double denominator = c.real * c.real + c.imag * c.imag;
        double r = (real * c.real + imag * c.imag) / denominator;
        double i = (imag * c.real - real * c.imag) / denominator;
        return Complex(r, i);
    }

    // Overload % operator
    Complex operator%(const Complex& c) const {
        double denominator = c.real * c.real + c.imag * c.imag;
        double r = (real * c.real + imag * c.imag) / denominator;
        double i = (imag * c.real - real * c.imag) / denominator;
        return Complex(real - c.real * r - c.imag * i, imag - c.real * i + c.imag * r);
    }

    // Display Complex number
    void display() const {
        std::cout << real << " + " << imag << "i" << std::endl;
    }
};

int main() {
    Complex c1(2, 3);
    Complex c2(4, 5);

    Complex c3 = c1 * c2;
    Complex c4 = c1 / c2;
    Complex c5 = c1 % c2;

    std::cout << "c1 = ";
    c1.display();

    std::cout << "c2 = ";
    c2.display();

    std::cout << "c1 * c2 = ";
    c3.display();

    std::cout << "c1 / c2 = ";
    c4.display();

    std::cout << "c1 % c2 = ";
    c5.display();

    return 0;
}

In the above example, we have overloaded the *, / and % operators using member functions. Here are some key points:

  • We define the operator*, operator/, and operator% functions as member functions of the Complex class.
  • We use the const keyword to specify that these functions do not modify the objects they are called on.
  • The operator* function returns a new Complex object that represents the product of two Complex objects.
  • The operator/ function returns a new Complex object that represents the quotient of two Complex objects.
  • The operator% function returns a new Complex object that represents the remainder of two Complex objects.
  • In the main function, we create two Complex objects c1 and c2, and then use the overloaded operators to perform arithmetic operations on them.
  • Finally, we display the results using the display function that we defined earlier.

Overall, by overloading these arithmetic operators as member functions, we can use them with Complex objects just like we would use the built-in arithmetic operators with primitive types.

Overloading assignment and compound assignment operators

Let’s continue with the complex numbers example we used before. The assignment operator, operator=, is used to assign one object to another object of the same class. By default, the compiler provides a default assignment operator which performs a shallow copy of the object. However, if the class contains any dynamically allocated memory, this default behaviour may not be sufficient and a custom assignment operator may need to be defined.

The default copy using the ‘=’ operator will only copy the pointer, not the actual data, resulting in two objects pointing to the same memory location. This can lead to problems if one of the objects is deleted, as the other object would be left with a dangling pointer.

To avoid such issues, we can define our own assignment operator that performs a deep copy of the object’s member variables, copying the actual data instead of just the pointer.

Syntax for overloading assignment operator for deep copying

class MyClass {
public:
  //...

  MyClass& operator=(const MyClass& other) {
    if (this != &other) { // check for self-assignment
      // Delete the existing resources held by the object
      // and copy the resources of the other object to this object
      // using deep copy
    }
    return *this;
  }

  //...
};

In this syntax, we define the assignment operator as a member function of the class MyClass and it takes a const reference to another object of the same class as its argument. Inside the function, we first check if the object being assigned to is not the same as the other object. If they are different, we delete the existing resources held by the object and copy the resources of the other object to this object using deep copy. Finally, we return a reference to the object itself.

Note that for deep copy, we need to define a copy constructor and a destructor that implement the logic of copying the resources held by an object and deleting them, respectively.

Example of overloading assignment operator for deep copying

Here is an example of how to overload the assignment operator for a Mystring class which is our minimal implementation of string class in C++:

#include <iostream>
#include <cstring>

class MyString {
private:
    char* str;
    int len;
public:
    MyString() : str(nullptr), len(0) {}
    MyString(const char* s) : str(nullptr), len(0) {
        if (s) {
            len = std::strlen(s);
            str = new char[len+1];
            std::strcpy(str, s);
        }
    }
    MyString(const MyString& other) : str(nullptr), len(other.len) {
        if (other.str) {
            str = new char[len+1];
            std::strcpy(str, other.str);
        }
    }
    MyString& operator=(const MyString& other) {
        std::cout << "Called overloaded method for assignment operator" << std::endl;
        if (this != &other) {
            delete[] str;    // Deallocate memory of the str string of current class to avoid memory leak/overflow/underflow
            len = other.len;
            if (other.str) {
                str = new char[len+1];
                std::strcpy(str, other.str);
            }
            else {
                str = nullptr;
            }
        }
        return *this;
    }
    ~MyString() {
        delete[] str;
    }
    const char* c_str() const {
        return str;
    }
};

int main() {
    MyString s1("hello");
    MyString s2("world");
    std::cout << "s1: " << s1.c_str() << std::endl;
    std::cout << "s2: " << s2.c_str() << std::endl;
    s1 = s2;
    std::cout << "s1 after assignment: " << s1.c_str() << std::endl;
    return 0;
}

Output:

s1: hello
s2: world
Called overloaded method for assignment operator
s1 after assignment: world

In this example, MyString is a class that holds a C-style string (char*) and its length. The constructor that takes a const char* argument allocates dynamic memory to hold the string and copies it into the class member. The copy constructor and destructor are also defined to handle dynamic memory allocation and deallocation.

The assignment operator overloading function is defined as a member function that takes a const MyString& argument and returns a reference to MyString. Inside the function, the code checks if the object being assigned to is not the same as the current object (i.e. it’s not a self-assignment). If it’s not a self-assignment, the function first deallocates the current memory pointed to by str and then allocates new memory for the incoming string, and copies the incoming string into the class member. Finally, it returns a reference to the current object.

The main() function demonstrates how the assignment operator can be used to assign one MyString object to another, and how the copy constructor ensures that dynamic memory is correctly allocated and deallocated for each object.

Difference between copy constructor and overloading assignment operator

The copy constructor and the overloaded assignment operator are two ways of copying one object to another, but they have some differences.

The copy constructor is used to create a new object by initialising it with an existing object. It is called automatically whenever a new object is created from an existing object, either by assignment or by passing the object by value.

On the other hand, the overloaded assignment operator is used to copy the contents of one object to another object that already exists. It is called explicitly by the user, using the assignment operator (=).

Here are some differences between the two:

  1. Parameters: The copy constructor takes a single parameter of the same class type as the object being copied, while the overloaded assignment operator takes a single parameter of the same class type as the object being copied to.
  2. Return value: The copy constructor does not return anything, while the overloaded assignment operator returns a reference to the object being assigned to.
  3. Initialization: The copy constructor initialises the new object using the values of the existing object’s data members, while the overloaded assignment operator copies the values of the existing object’s data members to the data members of the object being assigned to.
  4. Usage: The copy constructor is used when a new object is being created from an existing object, while the overloaded assignment operator is used when an existing object is being assigned a new value.

In summary, the copy constructor is used to create a new object, while the overloaded assignment operator is used to copy the contents of an existing object to another object. Both are important for creating and manipulating objects in C++.

Overloading assignment operator for moving

std::move()

Before jumping into this section of overloading assignment operator for moving, we need to understand the std::move() function. std::move is a utility function in C++ defined in the <utility> header. It is used to indicate that an object should be moved instead of copied. When an object is passed as an argument to std::move, it is converted into an rvalue, which makes it eligible for move operations.

The std::move function simply returns its argument as an rvalue reference, allowing the move constructor or move assignment operator to be called instead of the copy constructor or copy assignment operator. This can be useful when you want to move the contents of an object to a new location rather than copying it, which can be more efficient in certain situations.

For example, consider a vector of string objects. If you want to remove an element from the vector and move its contents to a new location, you can use std::move to move the contents instead of copying them:

std::vector<std::string> myVector = {"foo", "bar", "baz"};
std::string myString = std::move(myVector[1]); // move "bar" to myString
myVector.erase(myVector.begin() + 1); // remove "bar" from the vector

In this example, std::move is used to move the contents of the myVector[1] element to myString instead of copying them. This is more efficient than copying the contents because it avoids the overhead of allocating and deallocating memory twice.

Overloading assignment operator for move

When overloading the assignment operator, it is possible to distinguish between copying and moving an object. If an object is being moved, it means that the resources (such as dynamically allocated memory) owned by the source object are transferred to the target object, and the source object is left in a valid but unspecified state. This can be more efficient than copying the resources.

To overload the assignment operator for moving, we need to define a move assignment operator, which has the following syntax:

class MyClass {
public:
    // ...

    // move assignment operator
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            // move resources from other to this
            // ...

            // release resources from this
            // ...
        }
        return *this;
    }
    // ...
};

Do not worry about the keyword noexcept yet, we shall see it in detail in exception handling.

In the move assignment operator, the other object is an rvalue reference to the source object that is being moved. The noexcept specifier indicates that the function does not throw any exceptions.

Here is an example of a Vector class that has a move assignment operator:

#include <iostream>
#include <algorithm>

class Vector {
public:
    // default constructor
    Vector() : data(nullptr), size(0), capacity(0) {}

    // constructor with size and value
    Vector(int size, int value) : data(new int[size]), size(size), capacity(size) {
        std::fill_n(data, size, value);
    }

    // destructor
    ~Vector() {
        delete[] data;
    }

    // copy constructor
    Vector(const Vector& other) : data(new int[other.size]), size(other.size), capacity(other.size) {
        std::copy(other.data, other.data + other.size, data);
    }

    // copy assignment operator
    Vector& operator=(const Vector& other) {
        if (this != &other) {
            int* new_data = new int[other.size];
            std::copy(other.data, other.data + other.size, new_data);
            delete[] data;
            data = new_data;
            size = other.size;
            capacity = other.size;
        }
        return *this;
    }

    // move assignment operator
    Vector& operator=(Vector&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            capacity = other.capacity;
            other.data = nullptr;
            other.size = 0;
            other.capacity = 0;
        }
        return *this;
    }

    // ...

private:
    int* data;
    int size;
    int capacity;
};

int main() {
    Vector v1(3, 1);  // create a vector with size 3 and value 1
    Vector v2;        // create an empty vector

    v2 = std::move(v1);  // move v1 to v2

    std::cout << "v1 size: " << v1.size << '\n';  // should print 0
    std::cout << "v2 size: " << v2.size << '\n';  // should print 3

    return 0;
}

In the example, we create two Vector objects v1 and v2, and then move v1 to v2 using the move assignment operator. After the move, v1 has size 0 and v2 has size 3.

Overloading compound assignment operators

Compound assignment operators are operators that perform a binary operation and then store the result in the left-hand operand. For example, += operator performs an addition operation on its operands and stores the result in the left-hand operand.

In C++, we can overload compound assignment operators like +=, -=, *=, /=, etc. to work with user-defined classes. These operators are typically implemented as member functions.

The syntax for overloading compound assignment operators is similar to overloading the binary operator. Here is an example of overloading the += operator for a Vector class:

class Vector {
private:
    double *data;
    int size;
public:
    Vector(int size) : size(size) {
        data = new double[size];
        for (int i = 0; i < size; i++) {
            data[i] = 0.0;
        }
    }

    Vector(const Vector& other) : size(other.size) {
        data = new double[size];
        for (int i = 0; i < size; i++) {
            data[i] = other.data[i];
        }
    }

    ~Vector() {
        delete[] data;
    }

    // Compound assignment operator overloading
    Vector& operator+=(const Vector& other) {
        if (size != other.size) {
            throw std::invalid_argument("Sizes do not match");
        }
        for (int i = 0; i < size; i++) {
            data[i] += other.data[i];
        }
        return *this;
    }
};

In this example, we overload the += operator for the Vector class. The operator adds the corresponding elements of two Vector objects and stores the result in the left-hand operand. The function returns a reference to the left-hand operand to enable chaining of operators.

Here is an example of how to use the overloaded += operator:

int main() {
    Vector v1(3);
    v1[0] = 1.0;
    v1[1] = 2.0;
    v1[2] = 3.0;

    Vector v2(3);
    v2[0] = 4.0;
    v2[1] = 5.0;
    v2[2] = 6.0;

    v1 += v2; // Adds v1 and v2 and stores the result in v1

    // Outputs: [5, 7, 9]
    std::cout << v1 << std::endl;

    return 0;
}

In this example, we create two Vector objects v1 and v2. We add v2 to v1 using the overloaded += operator and store the result in v1. The result is [5, 7, 9].

Using the Complex class example, we can overload all the other compound assignment operators as follows:

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

class Complex {
private:
    double real, imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // compound assignment operator overloading
    Complex& operator+=(const Complex& rhs) {
        real += rhs.real;
        imag += rhs.imag;
        return *this;
    }
    
    Complex& operator-=(const Complex& rhs) {
        real -= rhs.real;
        imag -= rhs.imag;
        return *this;
    }
    
    Complex& operator*=(const Complex& rhs) {
        double temp_real = real * rhs.real - imag * rhs.imag;
        double temp_imag = real * rhs.imag + imag * rhs.real;
        real = temp_real;
        imag = temp_imag;
        return *this;
    }
    
    Complex& operator/=(const Complex& rhs) {
        double div = rhs.real * rhs.real + rhs.imag * rhs.imag;
        double temp_real = (real * rhs.real + imag * rhs.imag) / div;
        double temp_imag = (imag * rhs.real - real * rhs.imag) / div;
        real = temp_real;
        imag = temp_imag;
        return *this;
    }
    
    Complex& operator%=(const Complex& rhs) {
        real = fmod(real, rhs.real);
        imag = fmod(imag, rhs.imag);
        return *this;
    }
    
    // display function
    void display() {
        cout << real << " + " << imag << "i" << endl;
    }
};

// non-member functions for other compound assignment operators
Complex operator+(Complex lhs, const Complex& rhs) {
    return lhs += rhs;
}

Complex operator-(Complex lhs, const Complex& rhs) {
    return lhs -= rhs;
}

Complex operator*(Complex lhs, const Complex& rhs) {
    return lhs *= rhs;
}

Complex operator/(Complex lhs, const Complex& rhs) {
    return lhs /= rhs;
}

Complex operator%(Complex lhs, const Complex& rhs) {
    return lhs %= rhs;
}

Now, we can use the following code to test all the compound assignment operator overloads:

int main() {
    Complex c1(3.0, 4.0);
    Complex c2(2.0, 1.0);
    
    c1 += c2;
    c1.display();  // expected output: 5 + 5i
    
    c1 -= c2;
    c1.display();  // expected output: 3 + 4i
    
    c1 *= c2;
    c1.display();  // expected output: 2 + 11i
    
    c1 /= c2;
    c1.display();  // expected output: 3 + 4i
    
    c1 %= c2;
    c1.display();  // expected output: 1 + 0i
    
    return 0;
}

Output:

5 + 5i
3 + 4i
2 + 11i
3 + 4i
1 + 0i

Overloading operator using member functions

In all the examples we have seen so far, you might have observed that there are 3 ways in which we can overload a given operator: using member functions of the class, using global function and using a friend function. Let’s see it formally now.

Using member function

In this case, the left-hand operand is an object of the class, and the right-hand operand can be any compatible data type or another object of the class. The operator function is declared inside the class definition and is treated as a member function.

The syntax for overloading operators using member functions is as follows:

MyClass MyClass::operator+(const MyClass& rhs) const {
    // implementation
}

Or, if outside of the class:

In this example, the + operator is overloaded using a member function of the class MyClass. The function takes a reference to the right-hand operand as a constant reference (const MyClass& rhs) and returns a new object of the class MyClass.

Using global functions

Operator overloading using global functions involves defining the overloaded operator function as a global function outside the class. The syntax for overloading binary operators using global functions is as follows:

return_type operator symbol (operand_type1 operand1, operand_type2 operand2) {
    // code to perform the operation on the operands
}

Here, return_type is the type of the value returned by the operator function, symbol is the operator being overloaded, and operand_type1 and operand_type2 are the types of the two operands being operated on. The operator function takes two arguments of these operand types, which represent the two operands on which the operator is applied.

For example, to overload the + operator for a class MyClass using a global function, the syntax would be:

MyClass operator+ (const MyClass& obj1, const MyClass& obj2) {
    // code to perform the addition of obj1 and obj2
}

Here, MyClass is the return type of the overloaded operator function, + is the symbol for the addition operator, and const MyClass& obj1 and const MyClass& obj2 are the two operands being added, passed by reference to avoid copying. The operator function returns a new MyClass object that represents the result of the addition operation.

Using friend function

Operator overloading using friend functions allows a non-member function to access the private data members of a class, which is useful in cases where the operator function needs to modify the private data members directly. The syntax for operator overloading using friend functions is similar to overloading using global functions, with the addition of the friend keyword in the class declaration.

Here is an example of overloading the + operator using a friend function:

class MyClass {
private:
    int value;
public:
    MyClass(int v) : value(v) {}
    friend MyClass operator+(const MyClass& lhs, const MyClass& rhs);
};

MyClass operator+(const MyClass& lhs, const MyClass& rhs) {
    int sum = lhs.value + rhs.value;
    return MyClass(sum);
}

In this example, the operator+ function is declared as a friend function inside the MyClass class. This allows it to access the private data member value. The function takes two constant references to MyClass objects as its parameters, and returns a new MyClass object that contains the sum of the value data members of the two input objects.

Note that the operator+ function is defined outside of the class definition, and is declared as a global function. The friend keyword in the class declaration tells the compiler that this function has access to the private data members of the class.

Overloading stream insertion and extraction extraction

Stream insertion (<<) and extraction (>>) operators are used for input and output operations on streams, such as reading from or writing to files, or displaying output on the console. When we overload these operators, we can customise the way objects of our classes are displayed and read from the console or other input/output streams.

The advantages of overloading stream insertion and extraction operators are as follows:

  1. Readability: It makes the code more readable by allowing the use of standard input/output operations on user-defined types.
  2. Consistency: Overloading these operators for a class makes it consistent with other built-in types, which can also be input/output using these operators.
  3. Code reusability: We can define how the insertion and extraction of streams behaves once and that works the same every time. This can reduce and isolate errors.
  4. Flexibility: We can customise the input and output of the object according to our specific requirements.
  5. Ease of use: We can use the standard input/output stream classes (cin and cout) to input and output our objects, which makes the code shorter and more concise.

Overall, overloading stream insertion and extraction operators helps us to write more expressive and maintainable code, as well as providing a consistent and intuitive interface for interacting with user-defined types.

Let’s take an example to illustrate how overloading stream insertion and extraction operators help. Suppose we have a class called Person which has private members name and age.

class Person {
public:
    Person(std::string name = "", int age = 0) : name(name), age(age){} 
    std::string getName() const { return name; }
    int getAge() const { return age; }
    
private:
    std::string name;
    int age;
};

Inorder to read the name and age and create a Person’s object and then print the member values, we need to do something of the following sort:

int main() {
    std::string name;
    int age;
    std::cout << "Enter the name of the person: ";
    std::cin >> name;
    std::cout << "Enter the age of the person: ";
    std::cin >> age;
    Person p(name, age);
    std::cout << "The name of the person is " << p.getName() << " and his/her age is " << p.getAge() << std::endl;
    return 0;
}

If we were to overload the stream insertion and extraction operators, performing the above operation would be as simple as follows:

int main() {
    Person p;
    std::cin >> p;
    std::cout << p;
    return 0;
}

The syntax for overloading the stream insertion operator is as follows:

std::ostream& operator<<(std::ostream& out, const MyClass& obj) {
    // Code to output obj to out
    return out;
}

Here, std::ostream& is the return type of the function. The out argument is a reference to the stream object that we want to output to, and the const MyClass& argument is a reference to the object that we want to output.

The syntax for overloading the stream extraction operator is as follows:

std::istream& operator>>(std::istream& in, MyClass& obj) {
    // Code to input obj from in
    return in;
}

Here, std::istream& is the return type of the function. The in argument is a reference to the stream object that we want to input from, and the MyClass& argument is a reference to the object that we want to input into.

To demonstrate this practically, we can take up the same example of Person’s class:

#include <iostream>
#include <string>

class Person {
public:
    Person(std::string name = "", int age = 0) : name(name), age(age){}
    std::string getName() const { return name; }
    int getAge() const { return age; }
    
private:
    std::string name;
    int age;
};

// Overload stream insertion operator
std::ostream& operator<<(std::ostream& os, const Person& p) {
    os << "The name of the person is " << p.getName() << " and his/her age is " << p.getAge() << std::endl;
    return os;
}

// Overload stream extraction operator
std::istream& operator>>(std::istream& is, Person& p) {
    std::string name;
    int age;
    std::cout << "Enter the name of the person: ";
    is >> name;
    std::cout << "Enter the age of the person: ";
    is >> age;
    p = Person(name, age);
    return is;
}

int main() {
    Person p;
    std::cin >> p;
    std::cout << p;
    return 0;
}

Output:

Enter the name of the person: Prajwal
Enter the age of the person: 25
The name of the person is Prajwal and his/her age is 25

Rules of overloading stream insertion and extraction operators

  1. The stream insertion operator << must be overloaded as a non-static member function or a non-member function, while the stream extraction operator >> must be overloaded as a non-static member function only.
  2. Overloaded stream insertion and extraction operators must accept two arguments: the stream object on the left-hand side of the operator, and the object being read from or written to the stream on the right-hand side.
  3. The stream insertion operator << should return the stream object by reference, while the stream extraction operator >> should return the stream object by value or reference.
  4. Overloaded stream insertion and extraction operators should be declared as friend functions of the class whose objects are being inserted into or extracted from the stream. This is to allow the overloaded operator function to access the private members of the class.
  5. The overloaded stream insertion and extraction operators should be defined outside the class definition, typically in the same source file as the class implementation.
  6. Overloaded stream insertion and extraction operators should follow the same syntax and formatting conventions as the standard operators. For example, when overloading the stream insertion operator, the object being inserted should be written to the stream in a formatted manner.

By following these rules, we can ensure that the overloaded stream insertion and extraction operators behave consistently with the standard operators and provide a convenient and intuitive way to read from and write to streams.

The default keyword

In C++, the default keyword is used to explicitly specify that a member function (constructor, destructor, or assignment operator) should be generated by the compiler with its default behaviour. It is primarily used in relation to special member functions of a class.

When a special member function is defined with the default keyword, it means that the compiler-generated default implementation should be used for that function. The default behaviour is typically the one that would be automatically generated by the compiler if the function was not explicitly defined by the programmer.

Here are some use cases for the default keyword:

  1. Default Constructor: If a class needs a default constructor but also has other constructors defined, the default keyword can be used to explicitly request the compiler to generate the default constructor.
class MyClass {
public:
    MyClass() = default;  // Compiler-generated default constructor
    MyClass(int value) {
        // Constructor with parameter
    }
};
  1. Copy Constructor and Copy Assignment Operator: By using default, you can instruct the compiler to generate the copy constructor and copy assignment operator with their default behaviour.
class MyClass {
public:
    MyClass(const MyClass& other) = default; // Compiler-generated copy constructor
    MyClass& operator=(const MyClass& other) = default;  // Compiler-generated copy assignment operator
};
  1. Destructor: The default keyword can be used to explicitly define the destructor as the default destructor generated by the compiler.
class MyClass {
public:
    ~MyClass() = default;  // Compiler-generated destructor
};

Using the default keyword can be useful when you want to explicitly indicate that the default behaviour is desired, or when you want to make it clear that a default implementation is being used.

Note that the default keyword can only be used with special member functions, and it cannot be used for regular member functions or non-member functions.

What next?

This document has tried to provide explanations and examples to understand the basics of operator overloading, its need and contribution to writing good programs and its design language. It has left out redundant information such as describing the overloading of all the operators such as logical, comparison and others. Overloading those operators is also pretty much similar and should be fairly straightforward once you have understood the explanation and examples given in this document thoroughly.

It is strongly encouraged to practise overloading all the operators to get a good grip over the concept.

Exercises

Example 1: Fraction class

Create a Fraction class that contains two integer data members, numerator and denominator. Overload the following arithmetic operators using member functions: +, -, *, /, +=, -=, *=, /=, ++ (prefix), -- (prefix), ++ (postfix), -- (postfix). Also overload the stream insertion and extraction operators.

Example Usage:

Fraction f1(3, 4);
Fraction f2(1, 2);
Fraction f3 = f1 + f2;
std::cout << f3 << std::endl; // Output: 5/4

Fraction f4 = f1 * f2;
std::cout << f4 << std::endl; // Output: 3/8

f1 += f2;
std::cout << f1 << std::endl; // Output: 5/4

f1--;
std::cout << f1 << std::endl; // Output: 1/4

Exercise 2: Matrix

Create a Matrix class that contains a dynamic 2D array of integers and two integer data members, rows and columns. Overload the following arithmetic operators using member functions: +, -, *, /, +=, -=, *=, /=, ++ (prefix), -- (prefix), ++ (postfix), -- (postfix). Also overload the stream insertion and extraction operators.

Example Usage:

Matrix m1(2, 2);
Matrix m2(2, 2);
m1[0][0] = 1;
m1[0][1] = 2;
m1[1][0] = 3;
m1[1][1] = 4;
m2[0][0] = 4;
m2[0][1] = 3;
m2[1][0] = 2;
m2[1][1] = 1;

Matrix m3 = m1 + m2;
std::cout << m3 << std::endl; // Output: 5 5
                              //         5 5

Matrix m4 = m1 * m2;
std::cout << m4 << std::endl; // Output: 8 5
                              //         20 13

m1 += m2;
std::cout << m1 << std::endl; // Output: 5 5
                              //         5 5

m1--;
std::cout << m1 << std::endl; // Output: 4 4
                              //         4 4

Exercise 3

  1. Define a class Person with the following private member variables:
    • std::string name: stores the name of the person
    • int age: stores the age of the person
  1. Define the following public member functions for the Person class:
    • A constructor that takes in name and age as arguments and initialises the corresponding member variables.
    • A default constructor that initialises name to an empty string and age to 0.
    • void setName(const std::string& name): sets the name of the person.
    • std::string getName() const: returns the name of the person.
    • void setAge(int age): sets the age of the person.
    • int getAge() const: returns the age of the person.
  1. Overload the following comparison operators as member functions of the Person class:
    • bool operator==(const Person& other) const: returns true if the name and age of this Person object are the same as the name and age of the other Person object.
    • bool operator!=(const Person& other) const: returns true if the name and age of this Person object are not the same as the name and age of the other Person object.
    • bool operator<(const Person& other) const: returns true if the age of this Person object is less than the age of the other Person object. If the age is the same, then the name is used for comparison.
  1. Create several Person objects and test the overloaded comparison operators using if statements and cout statements. For example:
Person p1("Alice", 25);
Person p2("Bob", 30);

if (p1 == p2) {
    std::cout << "p1 and p2 are the same person\n";
} else {
    std::cout << "p1 and p2 are different people\n";
}

if (p1 < p2) {
    std::cout << "p1 is younger than p2\n";
} else {
    std::cout << "p1 is older than or the same age as p2\n";
}

Exercise 4

Create a class Person with the following private member variables:

  • name of type std::string
  • age of type int
  • income of type double

Define the following public member functions:

  • A default constructor that sets the name to an empty string, age to 0, and income to 0.
  • A constructor that takes in three parameters (name, age, income) and initialises the member variables accordingly.
  • Overload the == operator to compare two Person objects for equality based on all three member variables.
  • Overload the < operator to compare two Person objects based on their age.
  • Overload the && operator to return true if both Person objects have an income greater than 50000, and false otherwise.
  • Overload the || operator to return true if at least one Person object has an age greater than 50, and false otherwise.

In main(), create several Person objects and test out the overloaded operators to ensure they are working as expected.

Interview questions

The following are a finite set of possible interview questions that could be asked from operator overloading. The answer to some of the following interview questions might not be in this document but as said earlier in the “What’s next” section, this document provides enough data to streamline the understanding of the operator overloading. Answering the questions in this section will also help you broaden your understanding on this concept. The following are the questions:

  1. What is operator overloading, and why is it useful?
  2. What are the advantages and disadvantages of overloading operators using member functions vs global functions?
  3. Which operators cannot be overloaded in C++?
  4. What are the basic rules of operator overloading in C++?
  5. Can you explain the difference between the copy constructor and the assignment operator in C++?
  6. Can you provide an example of overloading the assignment operator for deep copying?
  7. How can you overload arithmetic operators in C++?
  8. What is the difference between overloading the increment operator as a prefix vs postfix operator?
  9. Can you explain how to overload stream insertion and extraction operators in C++?
  10. What is the syntax for overloading the subscript operator in C++?
  11. How can you overload the function call operator in C++?
  12. What is the difference between the new and new[] operators in C++, and how can you overload them?
  13. How can you overload the comma operator in C++?
  14. Can the scope of an overloaded operator be limited to a particular class?
  15. Can you overload the ternary conditional operator in C++?
  16. What is the role of the const keyword in operator overloading?
  17. Can you overload the new and delete operators in C++?
  18. What is the difference between the unary and binary operators in C++?
  19. Can you overload the type conversion operator in C++?
  20. Can you provide an example of overloading the comparison operators to compare two objects of a user-defined type?
  21. How can you overload the member selection operator to access a specific member of a user-defined type?

Leave a Reply

Your email address will not be published. Required fields are marked *

You cannot copy content of this page