Defining operators

Operator overloads

The operators are, if we see their operands as arguments, similar to functions. Like functions, they can be overloaded. The overload of the operators with variables of primitive types as operands are already defined by default. Therefore, we can directly use them with variables of primitive types.

int a = 1, b = 2; float c = 7; b = c; // int operator=(int leftOperand, float rightOperand); a += b; // int operator+=(int leftOperand, int rightOperand); c *= b; // float operator*=(float leftOperand, int rightOperand);

As shown above, the header of an operator overload has a return type, is named with the keyword operator followed by the symbol of the operator and has arguments which are its operands (The first one is the left operand and the right one is the right operand). Operators having only one operand (!, ~, ...) only take one argument.

Operators with classes

However, except for the = and == operators, by default, no operator overloads are defined for objects of classes (we create) as operands. Therefore, if we try to use them, there will be a compilation error.

When we want our classes to be able to be used with some operators, we must define overloads of those operators and code the instructions that should be executed by them.

For some classes, some operators (Or all or them) does not make sense to be defined. For example, consider we define a class used to represent a car, which is named Car. It does not make much sense to multiply two cars together. There is therefore no reason to define a multiplication operator overload taking objects of type Car as operands.

By default, an overload of the operators = and == is created for each class we define, with references of objects of that class as both operands. That overload of the operator = has the effect to copy the value of each member of the right operand to the members of the left operand. It is therefore called the copy operator. That overload of the operator == tests the equality of each corresponding member and returns true if they are all equal and false otherwise. Its is, however, possible to redefine them to change their behavior.

Let us have a look at the following code:

Time.hpp

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
#ifndef TIME_HPP #define TIME_HPP class Time { public: Time(); Time(unsigned hours, unsigned minutes, unsigned seconds); void setHours(unsigned hours); void setMinutes(unsigned minutes); void setSeconds(unsigned seconds); unsigned getHours() const; unsigned getMinutes() const; unsigned getSeconds() const; void printToConsole(); private: unsigned _hours; unsigned _minutes; unsigned _seconds; }; #endif

Time.cpp

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
#include "Time.hpp" #include <iostream> Time::Time() : _hours(0), _minutes(0), _seconds(0) { } Time::Time(unsigned hours, unsigned minutes, unsigned seconds) : _hours(hours), _minutes(minutes), _seconds(seconds) { } void Time::setHours(unsigned hours) { _hours = hours; } void Time::setMinutes(unsigned minutes) { _minutes = minutes; } void Time::setSeconds(unsigned seconds) { _seconds = seconds; } unsigned Time::getHours() const { return _hours; } unsigned Time::getMinutes() const { return _minutes; } unsigned Time::getSeconds() const { return _seconds; } void Time::printToConsole() { std::cout << "_hours: " << _hours << ".\n"; std::cout << "_minutes: " << _minutes << ".\n"; std::cout << "_seconds: " << _seconds << ".\n"; }

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "Time.hpp" int main() { Time time1, time2(1, 2, 3); time1 = time2; // Works, that operator overload is defined by default. Time time3 = time1 + time2; /* Error: The addition operator overload with two objects of type Time as operands has not been defined. */ return 0; }

When the line: Time time3 = time1 + time2; is reached, there is an error because no overload of the addition operator, taking two objects of type Time as operand, is defined.

To make it work, we must define the operator overload which has the following header:

Time operator+(const Time & left, const Time & right);

The arguments are references because we do not want the operands to be copied each time an addition operation is executed. Since they are not modified by the operation, we declare the references as constant. If they were not constant, we could not use that operator overload with constant objects. That overload returns an object containing the result of the addition of the two operands. Since that object is created inside the function (An operator is a special kind of function), we can not return a reference to it. Therefore, the operation returns a temporary object that is a copy of it (Which will be optimized the way we/*/ saw before).

Defining operators as functions

Operators are defined and declared the same way as functions. Here we define the operator overload from above:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Time operator+(const Time & left, const Time & right) { // Object holding the result. Time result; /* Assigns the member _hours from result with the summation of the members _hours from the objects left and right. */ result.setHours(left.getHours() + right.getHours()); /* Assigns the member _ minutes from result with the summation of the members _ minutes from the objects left and right. */ result.setMinutes(left.getMinutes() + right.getMinutes()); /* Assigns the member _ seconds from result with the summation of the members _ seconds from the objects left and right. */ result.setSeconds(left.getSeconds() + right.getSeconds()); return result; }

Now we can use the addition operator to add together two objects of type Time:

1
2
3
4
5
6
7
8
9
10
11
int main() { Time time1(1, 2, 3), time2(1, 2, 3); // Now it works. Time time3 = time1 + time2; time3.printToConsole(); return 0; }

Defining operators as class members

When we define an operator overload inside a class, as a member, the first (left) operand is considered to be the class. An operator overload member of a class is defined/declared the same way as a method. Below, we redefine the addition operator from above, but as a member of the class Time:

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
#include <iostream> class Time { public: // Other method declarations omitted for conciseness. // Addition operator overload declaration. Time operator+(const Time & right) const; }; // Addition operator overload definition. Time Time::operator+(const Time & right) const { Time result; /* We can directly access the member variables because the operator is a method of the class. */ result._hours = _hours + right._hours; result._minutes = _minutes + right._minutes; result._seconds = _seconds + right._seconds; return result; } int main() { Time time1(1, 0, 30), time2(2, 45, 10); Time time3 = time1 + time2; time3.printToConsole(); return 0; }

Non-constant methods can not be called (used) from constant objects, so by making the operator overload constant, we allow it to be used with a constant object as left operand. Since the operator overload is a method of the class Time, we can, inside of it, directly access the private members of the objects of type Time.

Note that the following operator definition is equivalent to the one above:

Time Time::operator+(const Time & right) const { return Time(_hours + right._hours, _minutes + right._minutes, _seconds + right._seconds); }

In the example above, we defined an overload of the addition operator taking two operands of type Time. Let us define an overload taking an object of type Time as left operand and a variable of type unsigned a right operand:

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
#include <iostream> class Time { public: // Other method declarations omitted for conciseness. // Addition operator overload declaration. Time operator+(unsigned right) const; }; // Addition operator overload definition. Time Time::operator+(unsigned right) const { return Time(_hours+right, _minutes, _seconds); } int main() { Time time(3, 22, 7); time = time + 5; std::cout << "Hours: " << time.getHours() << ".\n"; std::cout << "Minutes: " << time.getMinutes() << ".\n"; std::cout << "Seconds: " << time.getSeconds() << ".\n"; return 0; }

The addition operator overload from above returns an object of type Time, holding the time of the left operand added with the number of hours represented by the right operand. Since the right operand is of primitive type, its value is taken as argument instead of a reference.

With the default == operator, the two objects as operands are considered to be equal if, and only if, all their members are equal. What if we want to omit the member _seconds and consider that two objects of type Time are equal if their respective members _hours and _minutes are equal? To do so, we must define the == operator and code its behavior.

Let us define one == and one += operator overload, at the same time:

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
#include <iostream> class Time { public: // Other method declarations omitted for conciseness. // Not constant, because the members of the left operand will be altered. const Time & operator+=(const Time & right); // The == overload should return a boolean. bool operator==(const Time & right) const; }; const Time & Time::operator+=(const Time & right) { _hours += right._hours; _minutes += right._minutes; _seconds += right._seconds; // Returns a reference to the left operand. return *this; } bool Time::operator==(const Time & right) const { return (_hours == right._hours && _minutes == right._minutes); } int main() { Time time1(1, 20, 32), time2(1, 20, 13); if(time1 == time2) std::cout << "The objects time1 and time2 are equal.\n"; time1 += time2; time1.printToConsole(); return 0; }

Because the value returned by the operator+= is the value of the left operand, which is not defined inside the operator, a reference to it is returned, to avoid the creation of a copy, instead of its value.

The [] operator

<p.Like the other operators, the [] operator can be overloaded.

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
#include <iostream> class IntArray { public: int & operator[](unsigned index) { return _array[index]; } private: int _array[9]; }; int main() { IntArray a; // Sets the value of the element _a[0], through the returned reference. a[0] = 84; a[1] = 9; a[5] = 32; // Prints the value of the first two elements of the member '_a' of the object a. std::cout << a[0] << " " << a[1] << std::endl; return 0; }

Calling a member operator from a pointer

Let us look at the main function below, which use the class IntArray from above:

1
2
3
4
5
6
7
8
int main() { IntArray a; IntArray *ptrA = &a; return 0; }

We could access the [] operator of a, through the pointer ptrA, by dereferencing ptrA:

1
2
3
4
5
6
7
8
9
10
11
12
int main() { IntArray a; IntArray *ptrA = &a; (*ptrA)[0] = 943; std::cout << a[0] << std::endl; return 0; }

The reason we must dereference it is that otherwise we would use the [] operator of the pointer instead of the one of the pointed object.

We can call an operator, through a pointer and without dereferencing it, by calling it as if it was a normal method:

1
2
3
4
5
6
7
8
9
10
11
12
int main() { IntArray a; IntArray *ptrA = &a; ptrA->operator[](0) = 327; std::cout << a[0] << std::endl; return 0; }

Advantage of defining operators as functions

When we define an operator overload as a class member, the left operand has to be an object of the class. What if we want to define an operator with a variable of primitive type as left argument? Or what if we want to define an operator with an object of a class to which we do no have access to its definition, like a class from a library, as left operand? In these cases, we must declare the operator overloads as functions rather than methods.

Above, we defined an overload of the addition operator with an object of type Time as left operand and a variable of type unsigned as right operand. In the following example, we define an overload of the same operator with a variable of type unsigned as left operand and an object of type Time as right operand:

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
#include "Time.hpp" /* Declaration of the overload of the addition operator. The declaration is not required in this case, it is there simply to show that operator declaration is done the same way as function declaration. */ unsigned operator+(unsigned hour, const Time & time); /* Definition of the overload of the addition operator. Here, the value returned is the result of the addition of the left operand with the member '_hour' of the right operand. */ unsigned operator+(unsigned hour, const Time & time) { return hour+time.getHours(); } int main() { Time time(4, 20, 30); unsigned hours = 5 + time; std::cout << hours << std::endl; return 0; }

If we wanted the two followings expressions to be equivalent: (5 + time) and (time + 5). We could have, instead, defined the addition operator overload from above as:

Time operator+(unsigned hour, const Time & time) { return time+hour; }

Note that the addition operator overload for a variable of type unsigned as left operand and an object of type Time as right operand must be defined for that operator to work.