Operator Overloading

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

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
3D art showing a fantasy character
Ryan McCombe
Ryan McCombe
Edited

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 of 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

Preview - Pass by Reference

Our operator works correctly, but there is a small change we could make to improve its performance.

With our current implementation, our operands are being copied into our function body, which is unnecessary.

Users familiar with other programming languages may recognize this behavior as passing by value, which C++ does by default.

We cover this in detail later in the course but for those more comfortable, we can change our parameters to be passed by reference by appending an ampersand, &, to their types in our function parameter list:

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

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
  };
}

Preview - Chaining Operators

In this lesson, we’ve focused on operators that are typically expected to return new objects, such as the + and * operators.

Some operators are expected to modify their operand in place. Examples include ++ and *=.

We’ve intentionally avoided these operators for now, as we need to learn some more advanced concepts first.

Specifically, we haven’t currently covered enough to make these operators work when they are chained together, for example:

--Position *= 2;

We have a second lesson on operator overloading later in the course, where we cover implementing more advanced operators once we’ve learned all the prerequisites

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: Structured Binding

The upcoming lesson will introduce structured binding. This feature, a part of C++17, offers a more concise and readable way to handle simple types, such as the Vector3 we’ve been using in this lesson.

We’ll cover:

  • Basics and syntax of structured binding.
  • Practical application and examples of structured binding.
  • Limitations on when it can be used.

Was this lesson useful?

Edit History

  • Added section on unary operators

  • Moved earlier in the course and refreshed content

  • First Published

Ryan McCombe
Ryan McCombe
Edited
Lesson Contents

Operator Overloading

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

3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
Classes and Structs
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 56 Lessons
  • Over 200 Quiz Questions
  • Capstone Project
  • Regularly Updated
  • Help and FAQ
Next Lesson

Structured Binding

This lesson introduces Structured Binding, a handy tool for quickly unpacking simple data structures
3D art showing a fantasy pirate character
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved