Operator Overloading
This lesson introduces operator overloading, a fundamental concept to create more intuitive and readable code by customizing operators for user-defined types
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
Structured Binding
This lesson introduces Structured Binding, a handy tool for unpacking simple data structures