Step by step introduction into operator overloading

This article explains operator overloading by comparing it to an ordinary function. It demonstrates overloading on an 'equal to' operator which compares two Skyscraper objects. Below is the header and source file of the original class Skyscraper (for the moment without operator overloading).

#ifndef SKYSCRAPER_H
#define SKYSCRAPER_H
#include <stdexcept>
#include <math.h>

class Skyscraper
{
public:
    Skyscraper(double height);
    auto height() const {return m_height;}

private:
    double m_height;
};

#endif // SKYSCRAPER_H
#include "skyscraper.h"

Skyscraper::Skyscraper(double height): m_height(height){
    if(m_height < 0)
        throw std::invalid_argument(
                "Height must be non-negative.");
}

Imagine a situation where you have two skyscrapers and would like to find out whether they have equal height. To accomplish this without any knowledge of operator overloading, the simplest thing to do is to write a function that takes two Skyscrapers as arguments and returns true if they are of equal height, false if they are not. We are going to denote this function areEqual() and place it into main.cpp. Notice the 'const' keyword in front of the function arguments. This means that neither of the Skyscraper objects can be modified inside the function body. Also note that due to the fact that we are dealing with doubles, we should make clear what we consider equal. In this case we compare the absolute value of the height difference with an epsilon 0.01, as we most likely do not care about tiny height differences after the second digit that comes after the comma.

auto areEqual(const Skyscraper& s1, const Skyscraper& s2){
    if(fabs(s1.height() - s2.height()) < 0.01)
        return true;
    else
        return false;
}

The same intention can be expressed in a more compact form as:

auto areEqual(const Skyscraper& s1, const Skyscraper& s2){
    return fabs(s1.height() - s2.height()) < 0.01;
}

To see what the function does, we instantiate both Skyscraper objects with different heights. You can see this in main.cpp below. If you run the code, the console will print "Not equal". If you change the dimension of s2 to 345.0, the program will print "Equal".

#include <iostream>
#include "skyscraper.h"

auto areEqual(const Skyscraper &s1, const Skyscraper &s2);

int main()
{
    auto s1 = Skyscraper {345.0};
    auto s2 = Skyscraper {351.0};

    auto scrapersEqual = areEqual(s1,s2);
    if(scrapersEqual)
        std::cout << "Equal\n";
    else
        std::cout << "Not equal\n";
}

auto areEqual(const Skyscraper& s1, const Skyscraper& s2){
    return fabs(s1.height() - s2.height()) < 0.01;
}

But imagine how nice it would be if we could handle the Skyscrapers the way we handle integers or doubles. Then we could just write if(s1 == s2) and the program would give us the correct reply. Unfortunately, if you try to do that with the current code (below), you will get a compilation error. The compiler does not understand what == means for our Skyscraper class.

int main()
{
    auto s1 = Skyscraper {345.0};
    auto s2 = Skyscraper {351.0};

    if(s1 == s2)
        std::cout << "Equal\n";
    else
        std::cout << "Not equal\n";
}

To make this possible, we will have to overload the operator == in our Skyscraper class. We do not need much to accomplish this. We only need to declare the overloading of the operator == in the header file skyscraper.h. In the first variant of the problem, we are going to place the procedure for operator overloading after the closing braces of the class, i.e. the procedure won't be a member of the Skyscraper class. I am using this non-member approach as this is both recommended by the C++ Core Guidelines C86, C161, as well as easier to read.

#ifndef SKYSCRAPER_H
#define SKYSCRAPER_H
#include <stdexcept>
#include <math.h>

class Skyscraper
{
public:
    Skyscraper(double height);
    auto height() const {return m_height;}

private:
    double m_height;
};

bool operator==(const Skyscraper &s1, const Skyscraper &s2);

#endif // SKYSCRAPER_H

Notice, that the function takes exactly the same arguments and has the same body as the original function areEqual() in main.cpp. What changed is the name of the function (areEqual vs operator==). Also notice that for operator overloading the naming matters and for the function to be recognized as operator overloading, the operator keyword followed by the operator itself is a must. Below is the file skyscraper.cpp.

#include "skyscraper.h"

Skyscraper::Skyscraper(double height): m_height(height){
    if(m_height < 0)
        throw std::invalid_argument(
                "Height must be non-negative.");
}

bool operator==(const Skyscraper &s1, const Skyscraper &s2){
    return fabs(s1.height() - s2.height()) < 0.01;
}

Now the main.cpp became just what we wished for at the start and the program compiles just fine.

#include <iostream>
#include "skyscraper.h"

int main()
{
    auto s1 = Skyscraper {345.0};
    auto s2 = Skyscraper {351.0};

    if(s1 == s2)
        std::cout << "Equal\n";
    else
        std::cout << "Not equal\n";
}

As you certainly noticed, in the previous section, the operator== wasn't a member of the Skyscraper class. There is another, though less recommended, possibility. You can make operator== member function of your class. That is what I am going to show you now. This part requires a bit more attention since there will be changes in arguments, as well as in the body of the method.

Firstly, let's look below at the definition of the operator==. Notice, that in this case, the operator== appears in the public section of the Skyscraper class. Also, notice that one of the parameters (s1) disappeared. Having only a single argument, what are we going to compare it with? The answer to that is that we are going to compare it with *this, the object whose method is being defined and which was previously denoted as 's1'. Just to remind you, 'this' is a pointer to the location that holds the object whose method is defined. '*this' is the object at the location 'this'. Notice the extra const after the argument list. That means that the procedure does not modify the object on which it is invoked, i.e what we previously referred to as 's1'.

#ifndef SKYSCRAPER_H
#define SKYSCRAPER_H
#include <stdexcept>
#include <math.h>

class Skyscraper
{
public:
    Skyscraper(double height);
    auto height() const {return m_height;}
    bool operator==(const Skyscraper &s2) const;

private:
    double m_height;
};
#endif // SKYSCRAPER_H

Below you can see the new code for the operator == in the file skycraper.cpp. Since this time, the overloaded operator== is a member function, we need to use the scope operator::. Also, observe that the following changed in the body of the function compared to the previous case:

  • s1 was replaced by (*this)
  • height() was replaced by m_height, because operator== is now a member function and can access the private members directly
#include "skyscraper.h"

Skyscraper::Skyscraper(double height): m_height(height){
    if(m_height < 0)
        throw std::invalid_argument(
                "Height must be non-negative.");
}

bool Skyscraper::operator==(const Skyscraper &s2) const {
  return fabs(this->m_height - s2.m_height) < 0.01;
}

There are some differences when trying to overload other types of operators, but this example should provide you with sufficient means to discover the differences yourself. In particular, now you will be able to feed a vector of Skyscraper objects into an std::find function without any problems.