In the previous lesson, we saw examples on how we could overload operators like +
and *
.
These are known as binary operators, as they have two operands - something to the left of the operator, and something to the right.
We've also seen examples of built-in types unary operators. Unary operators only have one operand.
Examples include the ++
operator to increment a number, and the !
operator to invert a boolean.
Unsurprisingly, we can also overload those operators on our custom types.
First, though, we should note something interesting - some operators can be either unary or binary.
-
OperatorThe -
symbol is that it could represent a binary or a unary operation.
For example, when used with two integers, it performs subtraction:
int x { 3 };
int y { 2 };
x - y; // 1
But we can use it with a single integer, it changes the sign:
int x { 3 };
-x; // 3
Lets see how we can make this operator work with our custom type.
As you may have predicted, we can overload unary operators in the same way we do binary operators. The only difference is our operator function will have one fewer parameter.
That means, as a standalone function, it will only have one parameter:
Vector3 operator- (const Vector3& a) {
return Vector3 { -a.x, -a.y, -a.z };
}
And as a member function, it would have no parameters:
struct Vector3 {
float x;
float y;
float z;
Vector3 operator- () {
return Vector3 { -x, -y, -z };
}
};
We have previously seen the increment operator ++
in action with built in types like integers. After running the following code, MyNumber
will be 6
:
int MyNumber { 5 };
MyNumber++;
Specifically, this is called the postfix increment operator, because the ++
appears after the operand. There is also the prefix increment operator:
int MyNumber { 5 };
++MyNumber;
After running the above code, MyNumber
will also be 6
, just like when we used the postfix operator.
The difference is the value that is returned from each operator. The postfix operator will return the value of the int
before it was incremented.
The prefix operator will return the value of the int
after it was incremented.
This is relevant if we're immediately using the operator within an expression, such as a call to a function.
In the following example, both of our integers will eventually have a value of 11
but, on line 4 we call MyFunction
with 10
whilst on line 5 we call it with 11
.
1int MyNumber { 10 };
2int AnotherNumber { 10 };
3
4MyFunction(MyNumber++);
5MyFunction(++AnotherNumber);
6
Overloading prefix operators work exactly like we've seen before. However, unlike with previous operators, something like ++
is expected to modify its operand.
Therefore, we will pass it as a reference that is not const
.
As a standalone function, we could do something like this:
Vector3 operator++ (Vector3& a) {
a.x++;
a.y++;
a.z++;
return a;
}
And as a member function:
struct Vector3 {
float x;
float y;
float z;
Vector3 operator++ () {
x++;
y++;
z++;
return Vector3 { x, y, z };
}
};
Something to note about the previous examples is that they could also use the prefix operator on our floats to implement our function more concisely:
Vector3 operator++ (Vector3& a) {
return Vector3 { ++a.x, ++a.y, ++a.z };
}
struct Vector3 {
float x;
float y;
float z;
Vector3 operator++ () {
return Vector3 { ++x, ++y, ++z };
}
};
These operators are updating our vector correctly, but they are then returning a copy of the vector.
Typically, we want to return the same vector that the operator updated. This allows us to chain operators - for example:
(++MyVector) *= 5;
We improve our operators later in this lesson.
How might we implement the prefix decrement operator? For example, --MyVector
should reduce all the coordinates in MyVector
by 1
, and return the updated vector.
When we have a function called operator++
, C++ has a very crude way of letting us specifiy that we're overloading the postfix operator.
We need to include an additional int
parameter in our function. This int
has no purpose, other than to tell the compiler that we're overloading the postfix operator. Conventionally, this int
isn't even given a name:
Vector3 operator++ (Vector3& a, int) {
// implementation here
}
Lets implement our operator. To make the postfix ++
operator work in the way people would expect, we want it to increment our variable, but to return the value of the variable before it was incremented.
As long as we can do this in a single expression, we can just rely on the fact that the integer postfix ++
works that way by default:
Vector3 operator++ (Vector3& a, int) {
return Vector3 { a.x++, a.y++, a.z++ };
}
Often though, our real world use cases won't be possible in a single line of code.
For those scenarios, we might want to start off by create a copy of our original object, modify the original object, and then return the copy:
Vector3 operator++ (Vector3& a, int) {
Vector3 originalValue { a };
a.x++;
a.y++;
a.z++;
return originalValue;
}
Lets see how we could create this as a member function. Remember, we need the additional int
parameter to flag that we're overloading the postfix operator:
struct Vector3 {
float x;
float y;
float z;
Vector3 operator++ (int) {
return Vector3 { x++, y++, z++ };
}
};
How might we implement the postfix decrement operator? For example, MyVector++
should increase all the coordinates in MyVector
by 1
, but return the a vector with the values being unmodified.
The fact that the postfix operators needs to return the object in its previous state often has some performance implications.
Generally, the postfix operator some additional resources to implement this behavior. Because of this, if we don't care about the return value of our operator, it is often recommended to use the prefix operators.
this
Pointer in C++So far, our ++
and --
operators have been modifying our original object, but then returning a copy of it. This is somewhat functional, but not ideal.
Lets imagine we wanted to chain operators. That might look something like this:
Vector3 MyVector { 1.0, 2.0, 3.0 };
(++MyVector) *= 2;
After we run this code, we might expect our vector to be { 4.0, 6.0, 8.0 }
.
Unfortunately, it will be { 2.0, 3.0, 4.0 }
.
Our ++
operator will increment it successfully. However, the ++
operator returns a copy rather than the original MyVector
. The *=
operator will then act on that copy, leaving MyVector
unaffected.
Fortunately, our class and struct functions have access to a special variable, called this
. This variable stores a pointer to the specific object that the function was invoked from.
struct Vector3 {
float x;
Vector3* GetPointer() {
return this;
}
Vector3 GetObject() {
// We can dereference the pointer to get to the object
return *this;
}
}
Vector3 MyVector { 5 };
Vector3* Pointer { MyVector.GetPointer() };
int Five { Pointer->x };
Within our class functions, this pointer does not tend to be useful. After all, class code can access the variables on our object directly.
However, the this
pointer is useful when our class functions are calling non-class functions, and those non-class functions need access to our object.
class Vector3 {
int x;
void Act() {
LogX(this);
}
};
void LogX(Vector3* Vector) {
cout << Vector->x;
}
Vector3 MyVector { 5 };
MyVector.Act();
Additionally, the this
pointer is often useful as a return value from our class methods. This allows code outside our class to get access to our objects.
This is exactly what we need to solve our chained operator problem. Our operator functions need to return a reference to themselves, so we update the return type to be Vector3&
.
To access the current object and return that reference, we use the this
pointer:
struct Vector3 {
float x;
float y;
float z;
Vector3& operator++ () {
x++;
y++;
z++;
return *this;
}
Vector3& operator*= (int Multiplier) {
x *= Multiplier;
y *= Multiplier;
z *= Multiplier;
return *this;
}
}
When we create our operators this way, they can be chained as expected. After running the following code, MyVector
will be { 4.0, 6.0, 8.0 }
Vector3 MyVector { 1.0, 2.0, 3.0 };
(++MyVector) *= 2;
Up next, we'll learn how to use automatic type deduction, which asks the compiler to figure out our data types for us!
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way