5.1. Operator Overloading#

We have studied function overloading, where we can define multiple functions with the same name but different parameter lists. Depending on the arguments used in a function call, the appropriate function is selected.

Similarly, C++ allows us to overload operators, such as +, =, == and <<. This means that we can define how operators such as + behave when they are used with objects of user-defined types (i.e., classes). This is known as operator overloading.

Why Overload Operators?

Operator overloading can make code more intuitive and easier to read. For example, consider a Complex class that represents complex numbers with real and imaginary values. By overloading the + operator, we can add two Complex objects using the familiar + syntax.

We will explore how to overload various operators within the context of the Complex class defined as follows:

Starting Point: The Complex Class

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

5.1.1. Overloading the + Operator#

We want to add two Complex objects using the + operator as we show in the following code snippet:

1Complex x(3.0, 4.0); 
2Complex y(5.0, 6.0); 
3Complex z;
4
5z = x + y;  // We want to add two Complex objects using the + operator

Currently, the + operator can add two integers or doubles, e.g. 2.0 + i, where i is an integer. Additionally, those who implemented the string class in C++ implemented the + operator to enable two strings to be “added” and produce a new string object with two strings concatenated, e.g. string s = "Hello" + " world".

Similarly, we need to implement or overload the + operator to add two Complex objects, i.e. we need to define what x + y means when x and y are Complex objects.

Step 1: Understand the function header

In the expression x + y;, the left-hand side of the + operator is the object of class Complex. This means we can interpret the x + y to be equivalent to x.operator+(y). The function name is operator+. It should be a member function of Complex class, since we invoke operator+ on x. The object y is passed as an argument to this member function.

operator-plus

Fig. 5.1 x + y is interpreted as x.operator+(y). The left-hand side of the + operator is the object x, on which operator+ of the Complex class is invoked. The right-hand side of the + operator is the object y, which is passed as an argument to the operator+ member function.#

To implement the operator+ member function, we need to consider the following:

  1. Input Argument Type: The operator+ member function should take one parameter of type Complex, which is the object on the right-hand side of the + operator. Since the left-hand side object is the one on which the member function is invoked, it does not need to be passed as a parameter.

  2. Return Type: The operator+ member function should return a newly created Complex object that represents the sum of the two Complex objects.

  3. Public or Private: The operator+ member function should be declared as a public member function of the Complex class to allow it to be accessed from outside the class.

Step 2: Implement the operator+ Member Function

We can implement the operator+ member function as follows:

operator-plus-implement

Fig. 5.2 We create a new Complex object temp to store the sum of the two Complex objects. We add the real and imaginary parts separately and return the temp object.#

We pass the Complex object y as a parameter to the operator+ member function and name it rhs, short for right-hand side.

We can access the real and img data members of the left-hand side object x (the object on which operator+ is invoked) directly using real and img since we are inside a member function of the Complex class. We access the real and img data members of the right-hand side object rhs using the dot operator, e.g. rhs.real and rhs.img.

Inside the function, we create a new Complex object temp to store the sum of the two Complex objects. We set the real of temp to the sum of the real of y (rhs.real) and the real of the left-hand side object x, which is just real since operator+ is invoked on x. We do the same for the imaginary part.

Finally, we return the temp object. The return type of operator+ is Complex, which matches the type of the object we are returning.

Return by value

operator+ returns a Complex object by value. This means that a copy of the temp object is returned to the caller. Returning by value is important because the scope of temp will end by the end of the function and it will be destroyed. We want to return a newly created Complex object that is a copy of temp object at the time of return to by used in the caller function, e.g. main function.

Step 3: Optimizations on the operator+ Member Function

We can make three main optimizations to the operator+ member function:

  1. Pass the parameter by reference: Passing the Complex object rhs by value creates a copy of rhs just for the operator+ function. This can be memory inefficient especially if Complex has many data members. Instead, we can pass it by reference to avoid making a copy of the object when the function is called. This means we change the parameter type from Complex rhs to Complex& rhs.

  2. Pass rhs as a const: We can ensure that the function does not modify the rhs object. This is important because rhs is the right-hand side object of the + operator, and we do not want to change its value when performing addition. To do this, we can declare the parameter as a const reference, i.e. const Complex& rhs.

  3. Make the function const: We can declare the operator+ member function as a const member function to indicate that it does not modify the state of the left-hand side object. This is done by adding the const keyword after the function parameter list, i.e. Complex operator+(const Complex& rhs) const.

optimize-operator-plus

5.1.2. Overloading the == Operator#

Before getting to the operator= or assignment operator, another common operator to overload is the equality operator ==. We can use this operator to compare two Complex objects for equality. For example, we want to be able to write the following code:


#include <iostream>
using namespace std;

class Complex { private: double real; double img; public: Complex() { real = 0; img = 0; } Complex(double r, double i) { real = r; img = i; } // Overload the + operator Complex operator+(Complex rhs) { Complex temp; temp.real = real + rhs.real; // Add the real parts temp.img = img + rhs.img; return temp; } // Overload the == operator bool operator==(const Complex& rhs) const { return (real == rhs.real) && (img == rhs.img); } }; int main(void) { Complex x(3.0, 4.0); Complex y(3.0, 4.0); if (x == y) { cout << "x is equal to y" << endl; } else { cout << "x is not equal to y" << endl; } return 0; }

We can overload the == operator to compare two Complex objects for equality. The == operator should return true if both the real and imaginary parts of the two Complex objects are equal, and false otherwise.

In line 24, we write the function header for the operator== member function. The left-hand side object is the one on which the member function is invoked, and the right-hand side object is passed as a const reference parameter. The return type of the function is bool, which indicates whether the two Complex objects are equal or not.

The function is declared as a const member function since it does not modify the state of the left-hand side object. It also receives the right-hand side object as a const reference to avoid making a copy and to ensure that it is not modified.

In line 25, we compare the real and img data members of the left-hand side object and the right-hand side object rhs.real and rhs.img using the == operator for double values. If both parts are equal, we return true; otherwise, we return false.

We use the overloaded == operator in line 32 to compare the two Complex objects x and y. Since both objects have the same real and imaginary parts, the output will be x is equal to y.

5.1.3. Overloading the = Operator#

In the previous example, we used two operators: + and = in z = x + y;, where z, x and y are Complex objects. We successfully overloaded the + operator to add two Complex objects. This returns a a new Complex object (let’s call it temp). Then, the assignment operator = is used to assign the value of temp to the object z.

It’s time to overload the assignment operator =: operator=. The good news is C++ provides a default assignment operator operator= for classes. It performs a member-wise assignment of data members from the right-hand side object to the left-hand side object. This means the data members of temp are copied to the corresponding data members of z.

Let’s implement the default assignment operator for the Complex class to understand how it works.

Step 1: Understand the function header

In the expression z = temp;,

  1. We can interpret the z = temp to be equivalent to z.operator=(temp).

  2. The function name is operator=.

  3. It should be a member function of Complex class, since we invoke operator= on z.

  4. The object temp is passed as an argument to this member function of type Complex.

The function name and parameter type are written as follows:

<<return-type>> operator=(Complex rhs);

In C/C++, the assignment operator = must return something that allows for chained assignments, e.g. z = y = x;. This statement is evaluated right to left, i.e. y = x x is assigned to y, then in z = y y is assigned to z. In C/C++ standard, we should return from operator= the original left-hand side object after the assignment. Therefore, the assignment operator = must return a reference to the left-hand side object to allow for this chaining, which is of Complex class type. We add the & symbol to indicate that we are returning a reference.

chained-operator=

Fig. 5.3 In chained assignments like z = y = x;, the expression y = x is evaluated first, and the result, which is the original y, is then assigned to z. To enable this chaining, the assignment operator = must return a reference to the left-hand side object, or the object on which we invoked the operator= member function.#

Putting it all together, the function header for the assignment operator = is as follows:

Complex& operator=(Complex rhs);

Step 2: Implement the operator= Member Function

We can implement the operator= member function by setting the data members of the left-hand side object to the corresponding data members of the right-hand side object rhs, i.e. real = rhs.real and img = rhs.img.

How can we get the reference to the left-hand side object? One way is to use the this pointer. The this pointer points to the object on which the member function is invoked. Therefore, in z.operator=(temp), this points to the object z. We can dereference the this pointer using *this to get the left-hand side object itself. this is of type Complex*, so *this is of type Complex. To return a reference to the left-hand side object, we use return *this;.

We can visualize each object and the this pointer as follows:

this-pointer

Fig. 5.4 The this pointer points to the object on which we invoke the function on, e.g. object z. We can dereference the this pointer using *this to get the left-hand side object itself.#

Below is the complete implementation of the operator= member function:

operator-equals-implement

Fig. 5.5 We copy the real and img data members from the right-hand side object rhs to the left-hand side object using the dot operator. Finally, we return a reference to the left-hand side object using *this.#

In the image above, we optimize the operator= member function by passing the right-hand side object rhs as a const reference to avoid making a copy and to ensure that it is not modified.

We didn’t make the operator= member function a const member function because it modifies the state of the left-hand side object.