loading...
Cover image for An Introduction to Overloading Operators (for Beginners by a Beginner)

An Introduction to Overloading Operators (for Beginners by a Beginner)

somedood profile image Basti Ortiz (Some Dood) ・6 min read

Recently, I started learning the beautiful and punishing programming language of C++. I am currently very invested in the arduous journey towards mastery. After all, it will eventually help me in my journey in becoming a game developer.

Coming from the relatively less difficult programming language of JavaScript, there were so many unfamiliar concepts to me such as memory management and header files. Of course, I was initially confused about pointers, as do all beginners. However, as I learned more about the language, I stumbled upon a concept that was so new, so strange, so powerful, and so useful that I just had to explain it for beginners in the point of view of a beginner himself.

By the end of this article, I hope to effectively explain the concept of operator overloading with simple use cases and examples—away from all the fancy terminology, syntax, technicalities, and formalities that do nothing but confuse beginners (like me). For that reason, I will purposefully leave out topics that do not necessarily relate to the topic at hand because I'm not even qualified to talk about them in the first place.

Just a quick note before moving on to the meat of the article, I will assume that, at the very least, you already have a grasp of some important features and concepts of C++ (the type system, namespaces, references, the const keyword, structs, classes, and member initialization lists). Otherwise, you might get confused by the code I write.

What is overloading?

To oversimplify the wordy technical definition, in strongly-typed programming languages, "overloading a function" allows a function to accept multiple sets of arguments of different types for different situations. In other words, the function reacts accordingly to the arguments that were passed in. It is best to explain this through an example.

#include <iostream>

// Accepts integers
int overloaded(const int& num) {
  return num;
}

// Accepts three integers
int overloaded(const int& num1, const int& num2, const int& num3) {
  return num1 + num2 + num3;
}

// Accepts booleans
int overloaded(const bool& someBoolean) {
  // The argument is not used in the function to prove that it is overloaded.
  return 100;
}

int main() {
  // The `overloaded` function can accept multiple types and arguments.
  int inputNumber = overloaded(5);
  int sum = overloaded(3, 6, 1);
  int inputBool = overloaded(false);

  std::cout << inputNumber << std::endl; // 5
  std::cout << sum << std::endl; // 10
  std::cout << inputBool << std::endl; // 100

  return 0;
}

This is an extremely trivial example, but it hopefully shows how overloading works. With this in mind, you can probably guess what it means to "overload operators."

Operators are just functions

In C++, operators can be thought of as syntactic sugar for functions. Since operators are essentially functions, that means it is possible to overload some operators.

To demonstrate this, let's say we have a struct that represents a complex number. This struct has two member properties that each represent the real and imaginary components of a complex number in the form a + bi. It also has a method that allows you to add two ComplexNumbers.

struct ComplexNumber {
  const double a, b;

  // Constructor
  ComplexNumber(const double& a, const double& b) : a(a), b(b) {}

  // Adding method
  ComplexNumber add(const ComplexNumber& z) const {
    const double sumA = this->a + z.a;
    const double sumB = this->b + z.b;
    return ComplexNumber(sumA, sumB);
  }
};

In the main function, we can now add two ComplexNumbers and print the result to the screen.

int main() {
  ComplexNumber z1(2, 5); // 2 + 5i
  ComplexNumber z2(4, 7); // 4 + 7i

  ComplexNumber sum = z1.add(z2);

  printf("%.2f + %.2fi", sum.a, sum.b); // "6.00 + 12.00i"

  return 0;
}

This is great and all, but having to chain the ComplexNumber#add method is too cumbersome. With operator overloading, we can make this code more readable and intuitive.

To do so, we use the operator keyword to tell C++ what operator we want to overload. In this case, we will be overloading the + operator by naming the overload method as operator+. Since we already have an add method in the struct, we can just use its implementation for the overload, as seen in the example below.

struct ComplexNumber {
  const double a, b;

  // Constructor
  ComplexNumber(const double& a, const double& b) : a(a), b(b) {}

  // Adding method
  ComplexNumber add(const ComplexNumber& z) const {
    const double sumA = a + z.a;
    const double sumB = b + z.b;
    return ComplexNumber(sumA, sumB);
  }

  // Overload the `+` operator.
  // This overload returns a `ComplexNumber`.
  // This method takes in a `ComplexNumber` as an
  // argument, which comes from the right-hand
  // side of the `+` operator.
  ComplexNumber operator+(ComplexNumber& z) const {
    return add(z);
  }
};

This allows us to use the + operator instead of having to chain the ComplexNumber#add method.

int main() {
  ComplexNumber z1(2, 5); // 2 + 5i
  ComplexNumber z2(4, 7); // 4 + 7i

  // Using the `+` operator in this case is
  // equivalent to saying:
  // z1.add(z2);
  // The right-hand side of the operator
  // is the input to the overload method.
  ComplexNumber sum = z1 + z2;

  printf("%.2f + %.2fi", sum.a, sum.b); // "6.00 + 12.00i"

  return 0;
}

As a general rule, the three statements below are logically equivalent if the operator overload is defined as a member method of a struct or a class.

// Chaining methods
z1.add(z2).add(z3).add(z4).add(z5);

// Overloading the `+` operator
z1 + z2 + z3 + z4 + z5;

// Adding parentheses to put emphasis on the
// left- and right-hand side of the `+` operator
((((z1 + z2) + z3) + z4) + z5);

Defining operator overloads from outside a struct or class

We can take operator overloading a step further by overloading the stream insertion operator (<<) for the cout class in the <iostream> library. In a way, our objective is to display a ComplexNumber object to the screen as a string. If you're familiar with either Java or JavaScript, this is like overriding the toString method of an object.

#include <iostream>

struct ComplexNumber {
  // ...
}

// Overload the `<<` operator.
// The first argument corresponds to the
// left-hand side of the operator, while
// the second argument corresponds to the
// right-hand side of the operator.
std::ostream& operator<<(std::ostream& stream, const ComplexNumber& z) {
  return stream << z.a << " + " << z.b << 'i';
}

In the main function, it is now possible to log a ComplexNumber object as a string. On top of that, we have the ability to "sum up" multiple ComplexNumber objects together since we overloaded its + operator. Also, since we implemented the + operator overload based on ComplexNumber#add, we can choose to "add" a number of ComplexNumber objects together by either chaining method calls or using the + operator. Either way, the output is the same.

int main() {
  ComplexNumber z1(2, 5);
  ComplexNumber z2(4, 7);
  ComplexNumber z3(1, 9);

  // These statements are logically equivalent
  ComplexNumber sumChain = z1.add(z2).add(z3);
  ComplexNumber sumOverload = z1 + z2 + z3;

  std::cout << sumChain << std::endl; // "7 + 21i"
  std::cout << sumOverload << std::endl; // "7 + 21i"

  return 0;
}

Great Power == Great Responsibility

"Overload all the operators" meme
As tempting as it is to overload every single operator in C++—and yes, it is possible—we must remember that, like most things in life, "great power comes with great responsibility." Operator overloading must be used sparingly and only when completely and absolutely necessary.

In the case of writing Math APIs and data structures, such as my ComplexNumber example, it is a necessary tool in making code more readable and intuitive in the long run. It is definitely a justifiable added layer of complexity to the code base (or API).

On the contrary, we can go even crazier with operator overloads by making the + operator subtract its operands. Needless to say, this is a very bad idea, but at least you know it's possible.

In conclusion, you wouldn't want to overload every operator out there because, let's face it, that's just a disaster waiting to happen. It also takes up too much of your time. However, if it's an experiment that you want to try, then go ahead and explore the depths of operator overloading. I encourage you to practice your C++ skills.

Overload responsibly.

Posted on by:

somedood profile

Basti Ortiz (Some Dood)

@somedood

Just some dood trying to make code work without bringing the Universe to its demise.

Discussion

markdown guide
 

Very good explanation.

Something further that will probably be worth adding, however...

Overloaded binary operators, like +, declared within your class will assume an object of that class's type is the left hand operand.

This gets to be very very important information when you consider using overloaded operators to allow adding an int to your ComplexNumber class. If you have this in your class...

ComplexNumber& operator+(int rhs) const {...}

Then you can do this...

//foo is a ComplexNumber
foo = foo + 6;

However, you cannot do this...

foo = 6 + foo;

This throws a lot of C++ beginners for a loop, because they naturally assume that the commutative property of + is assumed. (Worse, your end users will curse you for breaking the rules of math.)

Thus, you actually have to declare a second overloaded operator outside your class...

ComplexNumber& operator+(int lhs, ComplexNumber& rhs) {...}

If you've declared both of those overloaded operator functions, the commutative property is preserved.

foo = foo + 6;
foo = 6 + foo;

As a rule, you should always declare binary operators for differing operand types both ways, even for non-commutative operands like -...at least, unless there's some illogic with one of the arrangements. If 42 + foo works, then 42 - foo should as well, right?

It is for this reason we conventionally use the names lhs and rhs for our overloaded operator arguments. lhs means "left-hand side", and rhs means "right-hand side". This helps prevent confusion. You should always use these names for operator arguments, unless you have a very VERY VERY good reason to use some other names. In fact, I'd even say that if you think you have a good reason, I'd bet you fifty bucks you're mistaken. ;)

One other tip: we virtually always declare an external overloaded operator function as friend of the class it relates to. There are exceptions, but make it your automatic habit.

 

Thank you so much for pointing this out! I learned a lot from your comment. Will definitely take down notes here.