This lesson is a quick introductory tour of operator overloading within C++. It is not intended for those who are entirely new to programming. Rather, the people who may find it useful include:
It summarises several lessons from our introductory course. Those looking for more thorough explanations or additional context should consider completing Chapter 8 of that course.
As we’ve seen, operators such as +
and *=
can act upon data types built into the C++ language, such as int
. However, we can make our custom types interact with these operators too. This is referred to as operator overloading.
Operators are simply functions. For a function to act as an operator, we need to follow a specific naming convention.
For example, imagine we have a custom type called Vector
:
struct Vector {
float x;
float y;
float z;
};
We wanted to give Vector
objects the ability to be added to other Vectors using the +
operator, That operator would return a new Vector
. To implement this, we’d define a function as follows:
Vector operator+(const Vector& a,
const Vector& b){
return Vector{
a.x + b.x, a.y + b.y, a.z + b.z};
}
Objects of our Vector
type can now be added to other objects of that same type, using the binary +
operator. And, given our function’s return type is Vector
, that operator would return another Vector
object:
Vector a { 1.f, 2.f, 3.f };
Vector b { 1.f, 1.f, 1.f };
// { 2.f, 3.f, 4.f }
Vector Sum { a + b };
Operators can also be implemented as part of the struct or class that defines our type. Below, we give our Vector
objects the ability to be multiplied with a float
using the binary *
operator:
struct Vector {
float x;
float y;
float z;
Vector operator*(float Multiplier){
return Vector{
x * Multiplier,
y * Multiplier,
z * Multiplier
};
}
};
Vector a { 1.f, 2.f, 3.f };
// { 2.f, 4.f, 6.f }
Vector Result { a * 2.f };
Multiple operators can have the same function name. For example, operator-
can be a binary operation, ie, it operates on two objects. This would normally be used to represent arithmetic subtraction (eg VectorA - VectorB
)
But, -
can also be a unary operator, acting upon only one object. This would normally be used for negation (eg, -VectorA
)
The compiler can infer whether we are overloading a unary or binary operator from the number of parameters.
Here are some examples:
#include <iostream>
struct Vector {
float x;
float y;
float z;
Vector operator-(){
std::cout << "Unary - \n";
return Vector{-x, -y, -z};
}
Vector operator-(const Vector& Other){
std::cout << "Binary - \n";
return Vector{
x - Other.x,
y - Other.y,
z - Other.z
};
}
};
Vector operator+(const Vector& a){
std::cout << "Unary + \n";
return a;
}
Vector operator+(const Vector& a,
const Vector& b){
std::cout << "Binary + \n";
return Vector{
a.x + b.x, a.y + b.y, a.z + b.z};
}
int main(){
Vector A;
-A;
A - A;
+A;
A + A;
}
Unary -
Binary -
Unary +
Binary +
this
PointerFor many operators, such as ++
and *=
, it is expected that the operator will return a reference to the object it operated on. This is important for allowing operators to be chained on our object, in expressions such as (MyObject *= 2) *= 3
.
With statements like this, we want subsequent operators to act upon the original object, not a copy of it.
Our previous examples didn’t allow this. However, within a class or struct function, the this
keyword returns a pointer to the current object - that is, the object that the function or operator was called upon.
Therefore, we can use this to ensure our operators return the exact same object they were used on:
#include <iostream>
struct Vector {
float x;
float y;
float z;
Vector& operator*=(float Multiplier){
x *= Multiplier;
y *= Multiplier;
z *= Multiplier;
return *this;
}
};
int main(){
Vector A{1.f, 1.f, 1.f};
(A *= 2) *= 3;
std::cout << "x component: " << A.x;
}
x component: 6
Operators like --
and ++
can be used either before or after their operand, and each usage has a different behavior.
For example, ++MyInteger
would increment the integer and return it. MyInteger++
would increment the integer but return a different integer, which has the value of our original integer before it was incremented.
For our custom types, overloading the prefix ++
works as expected:
struct Vector {
float x;
float y;
float z;
// ++MyVector (Prefix Operator)
Vector& operator++(){
++x;
++y;
++z;
return *this;
}
};
Overloading the postfix operator has a slightly inelegant syntax. To let the compiler distinguish between which function handles the prefix operator and which handles the postfix, we add an extra unused int
parameter to the postfix handler:
#include <iostream>
struct Vector {
float x;
float y;
float z;
// ++MyVector (Prefix Operator)
Vector& operator++(){
std::cout << "Prefix ++ \n";
++x;
++y;
++z;
return *this;
}
// MyVector++ (Postfix Operator)
Vector operator++(int){
std::cout << "Postfix ++ \n";
Vector Original{x, y, z};
++x;
++y;
++z;
return Original;
}
};
int main(){
Vector A;
++A;
A++;
}
Prefix ++
Postfix ++
Later in this course, we cover a range of further abilities we can add to our custom types. This includes how we can:
()
to let our custom types act like functions (functors).<=
and !=
Operator overloading allows you to define custom behavior for operators when used with objects of your custom types.
By overloading operators, you can make your custom types more intuitive and easier to use. Key takeaways:
this
pointer can be used to return a reference to the current objectint
parameter to distinguish them from prefix operatorsDiscover operator overloading, allowing us to define custom behavior for operators when used with our custom types
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games