Operator Overloading
For the basic types we've seen so far, operations and operators are already defined.
For example, an integer-type variable can be used with operators like addition (+
), subtraction (-
), assignment (=
), or increment (++
).
These have well-known effects on the variable's value.
However, for the custom classes we create, these operators do not work by default.
C++ gives us the ability to define how operators should work for our own types — we just need to specify what should happen, for instance, in the case of addition.
This is called operator overloading.
Let’s take a look at how the addition operator works for integers!
What does the expression a+b
actually mean?
1 |
|
It’s important to notice that the values of a and b do not change, and a new “object” is created to store the result of the expression (in the case of 3+2, this would be 5, which is also an integer). So, the operation has a return value. In the case of integers, this return value is an integer as well. If we extend the above with the return type, it would look like this:
1 |
|
For our own class, the concept is similar — we just need to define what we want to add to what, and what the result should be.
If we take the Course
class and add a Student
to it, we get an extended course, which is also a Course
.
So the two parameters are: Course
and Student
(which in practice will be a constant reference).
The return type is Course
.
1 |
|
Since the behavior of the operator is closely tied to the Course
(as we have defined it), according to OOP principles, the operation should be placed inside the class itself!
We can write this method as a member function of the Course
class.
If it's a member function, then the first parameter (Course
) is already given — because when we call the operator+
function on an object, the object itself becomes the first parameter.
Therefore, we can omit it from the parameter list.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
This adds the given student to the Course
if there's still space.
Its semantics are similar to that of integer addition.
Usage looks like this:
1 2 3 4 5 6 7 8 |
|
With integers, if we want to add a value to a variable, we use addition like this:
1 |
|
1 |
|
operator+=
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
This implementation is quite similar, but it's important to highlight a few significant differences!
The first is that the return type is not Course
, but Course&
, and we do not create a new Course
object.
The reason for this is the following:
When we used operator+
, the operands remained unchanged, and a new object was created to store the result - completely independent of the two operands.
This is visualized in the diagram below, where adding two Course
objects results in a third Course
:
If we want to use this in the form c1 = c1 + c2;
, then the original object (c1
) will be overwritten by the result object.
The following two diagrams illustrate this:
In such cases, it's better to use operator+=
.
With this operator, the assignment is already included in the operation itself, meaning the original object is modified.
This is also why we need to return by reference - because we're still referring to the same object.
Adding a student works in the same way:
It's important to note that the original Course
was modified - the Student
was added to the course - but the Student
itself was not changed.
That's why the parameter could be passed as a const &
.
Since we treat operators as functions, their definition is determined not only by the operator's name but also by the parameter type.
In the Course
class, we can have two versions of operator+=
: one with a Student
parameter, and another with a Course
parameter.
The Course
class extended in this way looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
The +=
operator works similarly for Course
objects as it does for Student
objects:
Természetesen, a kurzushoz adott paraméter most sem módosul, így az lehet const &
.
Indexer Operator¶
The indexer operator is considered a special kind of operator. Its semantics are generally associated with arrays - used for accessing elements - though we can assign it any custom meaning.
To enable this, we need to overload the operator[](int)
method.
When accessing an element in an array, we can do multiple things. For example, we can simply read its value (without modifying it):
1 |
|
Or we can modify the value:
1 |
|
Since in the first case we're only interested in the value, we don't need to modify it.
In the second case, however, we want to modify the returned value - meaning we actually want to overwrite the element at index 2 inside the int_array
object.
We've already seen that we can indicate whether a function is allowed to modify the object using the const
keyword.
In the first case (read-only), we cannot modify the object, so we use const
; in the second case (write), we don't.
Of course, simply marking the method as const
is not enough - if there's a "loophole" in the return type, the object might still be altered.
To prevent this, we also need to adjust the return type accordingly.
This was already discussed in the section about const
.
Extending the indexer operator for the Course
class makes sense here, since Course
contains an array-like structure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
In the first case, we return a reference, so wherever the return value is used, it gives direct access to the specific element - meaning its value can even be overwritten.
Since this can modify the Course
, the function itself cannot be marked as const
.
In the second case, we also return a reference, but this time it’s a const &
, so the value cannot be overwritten.
This guarantees that no matter what is done with the returned reference, the Course
will not be modified - which is why this function can be marked as const
.
++ / -- Operators¶
The expressions ++a
, a++
, --a
, and a--
are familiar to most.
The difference between prefix and postfix increment (or decrement), especially for integers, is that:
- In the prefix version, the expression uses the already incremented/decremented value.
- In the postfix version, the expression still uses the original (non-incremented/decremented) value when evaluated.
Since these are also operators, they can be overloaded as well. However, there's not much syntactic difference between the prefix and postfix versions, so we need a way to distinguish them in the implementation.
When overloading, the postfix version takes an extra (unused) parameter - typically an int
- just to differentiate it from the prefix version.
Because of their differing behavior, their return types also differ slightly.
Let’s look at an example!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
It is useful to implement both together, and in that case, one can be implemented using the other. This way, when fixing bugs, you only need to modify the code in one place.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
Further Operator Overloading¶
As we saw before, operator overloading simply means implementing a function or method.
When we implement an operator as a method of a class, we can omit the first parameter of the operator because it implicitly refers to the object itself.
Naturally, if the operator is unary (i.e., it takes only one parameter), then no parameters need to be specified at all, since only the object is required.
Examples of such operators include operator++
, operator--
, operator!
, etc.
operator!¶
The exclamation mark operator (!
) often represents negation.
It takes one parameter - the object itself.
Staying with the Course
example, we can consider that some courses require approval.
We can store this as a bool
value, but if we want to change it, we can also do so using negation.
Let’s look at an example!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
Conversion Operators¶
Often we convert data, and sometimes we need to do this for a class we've created. We could write a function that always performs the conversion, but it's more convenient to specify how a certain conversion should be done.
For example, if we want to convert a Course
object to an int
, where the conversion result is the number of enrolled students, we can implement this with a conversion operator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
We can see in the above example that we converted our course object to an unsigned type for output.
This was done using the conversion operator defined inside the Course
class.
Important: For conversion operators, you do not need to specify the return type! This is because the conversion operator's name clearly determines the return type - it is the type you want to convert to. If you use a different type, the compiler will generate an error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
In this example, we can see that the conversion does not need to be explicitly written; it happens automatically. However, this can lead to problems because sometimes we may not want the conversion to happen, or we might want to convert to a different type instead.
A solution to this is to make the conversion only possible through explicit casting. This means that the conversion must be explicitly stated, preventing automatic conversions.
To achieve this, we mark the conversion operator with the explicit
keyword.
This disables implicit conversions and requires explicit casting for the conversion to happen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Conversion in the Other Direction¶
So far, we have looked at how to convert an existing object into a value via conversion operators. The other direction is how to create an object of a given type from a value.
An example of this can be seen with strings:
1 2 3 4 5 6 7 8 9 |
|
We can do this with our own class as well. Since we need to create an object based on a value, we have to look at the parts related to the constructor!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Output
5
0
5
We can see that the c3
object can be initialized with an unsigned value because there is a matching constructor.
Naturally, there may be cases when we want to avoid this automatic conversion (constructor call).
In such cases, we can mark the constructor as explicit so that it can only be called explicitly.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Created: 2025-09-04