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.
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:
operator+
int
as the first argumentint
as the second argumentWith 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
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
};
}
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;
}
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 };
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.
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 };
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:
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 };
}
};
How would we overload the unary -
operator using a standalone function?
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
};
}
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
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:
operator
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:
This lesson introduces operator overloading, a fundamental concept to create more intuitive and readable code by customizing operators for user-defined types
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way