Operator Overloading

This lesson introduces operator overloading, a fundamental concept to create more intuitive and readable code by customizing operators for user-defined types

Ryan McCombe
Updated

In the previous lesson, we created a simple Vector3 struct to hold 3 numbers, which we could use to represent concepts like a position in a 3D world:

struct Vector3 {
  float x;
  float y;
  float z;
}

It would be nice if we were able to use operators like + and += with our new custom type, in much the same way we were able to do with built-in types like int and float.

For example, we would like to be able to do things like this:

Vector3 CurrentPosition { 1.0, 2.0, 3.0 };
Vector3 Movement { 4.0, 5.0, 6.0 };

// Create a new object using the + operator
Vector3 NewPosition { CurrentPosition + Movement };

After running the above code, we'd want NewPosition to be a Vector3 with the values 5.0, 7.0, and 9.0.

For this, we need to overload operators.

Operators are Functions

In C++, operators are simply functions that have a specific name and parameter list.

When we write code like 1 + 2, the compiler is going to try to call a function with the following properties:

  • Has the name of operator+
  • Accepts an int as the first argument
  • Accepts an int as the second argument

With that in mind, the function prototype that allows two our our Vector3 objects to use the + operator could have this prototype:

void operator+(Vector3 a, Vector3 b);

It is up to us to define what an operator does, and what it returns. Naturally, when we see +, we'd expect that operator to add the two operands together, and return the result.

So let's update the function's return type and body to do that:

Vector3 operator+(Vector3 a, Vector3 b){
  return Vector3{
    a.x + b.x,
    a.y + b.y,
    a.z + b.z
  };
}

With that, we can now easily add our Vector3 objects together:

#include <iostream>
using namespace std;

struct Vector3 {
  float x;
  float y;
  float z;
};

Vector3 operator+(Vector3 a, Vector3 b){
  return Vector3{
    a.x + b.x,
    a.y + b.y,
    a.z + b.z
  };
}

int main(){
  Vector3 CurrentPosition{1.0, 2.0, 3.0};
  Vector3 Movement{4.0, 5.0, 6.0};
  Vector3 NewPosition{
    CurrentPosition + Movement
  };

  std::cout
    << "x= " << NewPosition.x
    << ", y = " << NewPosition.y
    << ", z = " << NewPosition.z;
}
x= 5, y = 7, z = 9

Operand Order Matters

C++ does not consider an expression like A * B to be equivalent to B * A.

This has implications for our operator overloading. For example, if we wanted to give our Vector3 the ability to be multiplied by an int, we typically need to implement two variations.

We need to implement one where the int is the left operand, to support expressions like 2 * MyVector:

// int * Vector3
Vector3 operator*(int num, Vector3 vec) {
  return Vector3{
    vec.x * num, vec.y * num, vec.z * num
  };
}

And we need to implement a variation where the int is the right operand, to support expressions like MyVector * 2:

// Vector3 * int
Vector3 operator*(Vector3 vec, int num) {
  return Vector3{
    vec.x * num, vec.y * num, vec.z * num
  };
}

In situations like this, where the parameter order doesn't change the output of our operation (that is, our operation is commutative), we can implement one function in terms of the other.

That is, if someone calls the Vector3 * int variation, we simply defer to the int * Vector3 implementation:

// int * Vector3
Vector3 operator*(int num, Vector3 vec) {
  return Vector3{
    vec.x * num, vec.y * num, vec.z * num
  };
}

// Vector3 * int
Vector3 operator*(Vector3 vec, int num) {
  return num * vec;
}

Test your Knowledge

Operator Overloading

What function prototype would we need to allow two Vector3 objects to be subtracted using the - operator, returning a new Vector3? For example:

Vector3 CurrentPosition { 1.0, 2.0, 3.0 };
Vector3 Reverse { 4.0, 5.0, 6.0 };

Vector3 NewPosition{CurrentPosition - Reverse};

What function would we need to allow a Vector3 to be multiplied by an int using the * operator? For example:

Vector3 CurrentPosition { 1.0, 2.0, 3.0 };

Vector3 NewPosition { CurrentPosition * 5 };

Overloading Operators with Member Functions

The previous examples implemented operator overloading using a standalone function, outside of any class or struct. These are sometimes referred to as free functions.

However, it is also possible to implement operators as a member function, within the relevant class or struct.

Let's see what the + operator might look like using that method:

struct Vector3 {
  float x;
  float y;
  float z;

  Vector3 operator+ (Vector3 Other) {
    return Vector3 {
      x + Other.x,
      y + Other.y,
      z + Other.z
    };
  }
};

The key thing to note in the declaration of operator+ as a member function is that there is only one parameter.

This might be confusing, as the + operator has two operands - a left and a right. When we created this as a free function earlier, we needed to have two parameters:

Vector3 operator+ (Vector3 a, Vector3 b);

However, when we overload the operator as a member function, the function is called within the context of the left operand. So for example, an expression like x is accessing the x member of the left operand.

Therefore, there is no need to have the left operand be provided as an argument - we can just access its state in the same way we would from any other class function.

Test your Knowledge

Operator Overloading as a Member Function

How can we allow our Vector3 objects to be multiplied with a float using a member function operator overload? For example:

struct Vector3 {
  float x;
  float y;
  float z;

  // Add a function here
};

Vector3 MyVector { 4.0, 5.0, 6.0 };
Vector3 BigVector { MyVector * 3.0 };

Unary Operators

The previous sections are all examples of binary operators. Binary operators have two operands - a left, and a right.

// Add LeftOperand and RightOperand
LeftOperand + RightOperand;

Some operators take only one operand - these are called unary operators. ++ is an example of a unary operand.

// Increment SomeNumber
SomeNumber++;

Some symbols, such as - can be used either as a unary or binary operand. The unary - is generally used to get the negative form of an operand, whilst the binary - is used to subtract the right operand from the left:

int Number{5};

-Number; // Return -5
Number - Number; // Return 0

We implement unary and binary operators in exactly the same way. The only difference is the number of parameters our function will have:

  • Overloading a binary operator as a standalone function will use 2 parameters
  • Overloading a binary operator as a member function will use 1 parameter
  • Overloading a unary operator as a standalone function will use 1 parameter
  • Overloading a unary operator as a member function will use no parameters

Below, we overload the unary - operator using a member function:

struct Vector3 {
  float x;
  float y;
  float z;
  
  Vector3 operator-() {
    return Vector3 { -x, -y, -z };
  }
};

Test your Knowledge

Overloading Unary Operators

How would we overload the unary - operator using a standalone function?

Operator Prototypes and Definitions

Like any other member function, we can declare and define our prototypes in different locations, if we prefer:

struct Vector3 {
  float x;
  float y;
  float z;

  // prototype
  Vector3 operator+ (const Vector3& Other);
};
// definition
Vector3 Vector3::operator+ (Vector3 Other) {
  return Vector3 {
    x + Other.x,
    y + Other.y,
    z + Other.z
  };
}

Summary

In this lesson, we introduced operator overloading, allowing us to extend the capability of our user-defined types, and interact with them in more expressive ways.

The key learnings are:

  • Operators are implemented as simple functions in C++, with a specific naming convention using the word operator
  • Operator overloads can be implemented using free functions or member functions, and which approach we choose has implications on what our function parameters will be
  • Operators can be defined and declared in different locations, just like any other function
Next Lesson
Lesson 25 of 60

Structured Binding

This lesson introduces Structured Binding, a handy tool for unpacking simple data structures

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Why Use Operator Overloading?
Why do we need operator overloading when we could just create regular functions like AddVectors() or MultiplyVectors()?
Overloading With Different Types
Can we overload operators to work with different types? For example, could I multiply a Vector3 by a float instead of just an int?
Parameter Order in Operators
Does the order of parameters matter in operator overloading? Like, is (Vector3, int) different from (int, Vector3)?
Unconventional Operator Behavior
Can I overload operators to do completely different things than what they normally do? Like making + do multiplication?
Operators Across Different Types
Can I use operator overloading with custom types from different classes? Like adding a Vector3 to a Point3D?
Creating Custom Operators
Can I create new operators that don't exist in C++? Like ** for exponentiation?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant