There are many times when operator overloading provides useful abstraction both inside a class and outside. Because operators may have several signatures, I provide this article to help clear up the details. The keyword operator can be used to define a type-conversion member function. It is also used to overload the built-in C operators. Just as a function name can be given a variety of signatures, with each signature having a slightly different implication and meaning, so can the operators, such as +, be given additional meanings. Overloading operators allows writing infix expressions of both user-defined types (classes) and built-in types.
The use of operator overloading often results in shorter and more readable programs.
To provide better insight into operator overloading, consider the idea of adding the product of two numbers A and B to the product of two others, C and D, and storing this in a RESULT. The programming language could have provided simple built-in functions to accomplish this:
Then we would write something like:
However, it is much easier to understand:
Note: If you are wondering about the variable names lhs and rhs, wonder no more. LHS is shorthand for the "Left-hand side." RHS is shorthand for the "Right-hand side." This nomenclature is evident when you use the simple notion:
Thus the C++ compiler automatically parses the above and then calls functions. The names of the functions are a bit odd but easily understandable:
It is technically legal to write the following hard to understand code that is effectively just like the initially proposed solution:
This article further discusses some of the subtleties of operator overloading.
All C++ operators can be overloaded except for the following:
Those that can be overloaded are:
Additionally, there is a restriction that some operators are only overloadable in the context of a class. These are:
Although C++ enables the programmer to redefine the meaning of most of the built-in operator symbols for a class, there is no ability to change the precedence rules that dictate the order in which operators evaluate. C++ operators have the same precedence as those of their ANSI C counterparts. Even if, for some class, the programmer were to define operators + and * to have entirely different meanings from addition and multiplication, in an expression such as this:
C++ still invokes the operator* function to evaluate b * c before calling operator+.
The following tips should help in designing classes with overloaded operators (assume that a and b are instances of appropriate class types).
All assignment operators are restricted to use inside class definitions. Additionally, as of the C++ 2011 standardization, there are two types of assignment operations: copy assignment and move assignment. This twist makes the topic slightly more complicated than it used to be; however, the benefits can be enormous.
Several different signatures are allowed for the copy assignment operator:
These signatures permute both the return type and the parameter type. While the return type may not be too significant, the choice of the parameter type is critical.
(2), (5), and (8) pass the right-hand side by non-constant reference and is discouraged. The problem with these signatures is that the following code would not compile:
This problem is because the right-hand side of this assignment expression is a temporary (un-named) object, and the C++ standard forbids the compiler to pass a temporary object through a non-const reference parameter.
This result leaves us with passing the right-hand side either by value or by const reference. Although it would seem that passing by const reference is more efficient than passing by value, we show later that for reasons of exception safety, making a temporary copy of the source object is unavoidable. Therefore passing by value allows us to write fewer lines of code.
A reference should be returned by the assignment operator to allow operator chaining. You typically see it with primitive types, like this:
The compiler interprets this code as:
In other words, the assignment is right-associative. The last assignment operation is evaluated first and is propagated leftward through the series of assignments. Specifically:
Now, to support operator chaining, the assignment operator must return some value. The proper return value should be a reference to the left-hand side of the assignment.
Notice that the returned reference is not declared const. This reference can confuse because it allows you to write crazy stuff like this:
At first glance, you might want to prevent situations like this, by having operator= return a const reference. However, statements like this work with primitive types. And, even worse, some tools rely on this behavior. Therefore, it is important to return a non-const reference from your operator=. The rule of thumb is, "If it's good enough for `ints, it's good enough for user-defined datatypes."
So, for the hypothetical My_class assignment operator, do something like this:
Remember, this is a pointer to the object that the member function operates upon. Because a = b becomes a.operator=(b), you can see why it makes sense to return the object that the function operates upon; object a is the left-hand side.
But, the member function needs to return a reference to the object, not a pointer to the object. So, it returns *this, which returns what this points at (i.e., the object), not the pointer itself. (In C++, instances become references, and vice versa, pretty much automatically, so even though *this is an instance, C++ implicitly converts it into a reference to the instance.)
A critical point about the assignment operator:
YOU MUST CHECK FOR SELF-ASSIGNMENT!
This point is especially important when your class manages memory allocation. Here is why: The typical sequence of operations within an assignment operator is usually something like this:
Now, what happens when you do something like this:
You can hopefully see that this would wreak havoc on your program. Because my_object is on both the left-hand side and on the right-hand side, the first thing that happens is that my_object releases any memory it holds internally. But doing so destroys the data of the right-hand side needed for copying! So, you can see that this completely messes up the rest of the assignment operator's internals.
The easy way to avoid this is to CHECK FOR SELF-ASSIGNMENT. There are many ways to answer the question, "Are these two instances the same?" But, for our purposes, just compare the two objects' addresses. If they are the same, then don't do the assignment. If they are different, then do the assignment.
So, the correct and safe version of the My_class assignment operator would be this:
Or, you can simplify this a bit by doing:
Remember that in the comparison, this is an immutable reference (constant pointer) to the left-hand object of the comparison, and &rhs is a reference to the object passed as the right-hand argument. So, you can see that we avoid the dangers of self-assignment with this check.
In summary, the guidelines for the assignment operator are:
Now for a small twist. In 2011, C++ introduced a new concept known as move assignment, and that is the purpose of the following signature.
The idea of move assignment is for the compiler to simply move a pointer rather than copy a bunch of data if and only if the right-hand side is a temporary that would be going away (destroyed). Using movement turns a copy and destruct combination into an inexpensive move operation.
The move assignment operator has an extra task, which is to invalidate the right-hand side.
I discuss these before the arithmetic operators for a particular reason explained later. The critical point is that compound assignment operators are destructive operators because they update or replace the values on the left-hand side of the assignment. So, you write:
In this case, the operator+= modifies the values.
How those values are modified isn't important. The class My_class dictates what these operators mean.
The member function signature for such an operator should be like this:
We have already covered the reason why rhs is a const-reference. And, the implementation of such an operation should also be straightforward.
Notice the operator returns a 'My_class-reference`, and a non-const one at that. You can now do things like this:
Don't ask me why somebody would want to do this, but just like the standard assignment operator, this is allowed by the primitive data types. Our user-defined datatypes should match the same general characteristics of the primitive data types when it comes to operators, to make sure that everything works as expected.
The coding is very straightforward to do. Just write your compound assignment operator implementation, and return *this at the end, just like for the regular assignment operator. So, you would end up with something like this:
As one last note, in general, you should beware of self-assignment with compound assignment operators as well.
The binary arithmetic operators are interesting because they don't modify either operand. They return a new value derived from the two arguments. You might think this is going to be an annoying bit of extra work, but here is the secret:
Define your binary arithmetic operators using your compound assignment operators.
This approach just saved you a bunch of time.
So, you have implemented operator+=, and now you want to implement operator+. The function signature should be like this:
The above code explicitly spells out all of the steps, and if you want, you can combine them all into a single statement, like so:
This code creates an unnamed instance of My_class, which is a copy of *this. Then, operator+= gets called on the temporary value and then returns it.
If that last statement doesn't make sense to you yet, then stick with the other way, which spells out all of the steps. But, if you understand what is going on, then you can use that approach.
Notice that the + operator returns a const instance, not a const reference. Using a constant prevents people from writing strange statements like this:
This statement does nothing useful, but if the + operator returns a non-const value, it compiles without error! So, we want to return a const instance, so that such foolishness will not even be allowed to compile.
To summarize, the guidelines for the binary arithmetic operators are:
The comparison operators are straightforward. Define == first, using a function signature like this:
The internal code is self-explanatory and straightforward. Even the bool return-value is easily understood.
The critical point here is that we can define the != operator in terms of the == operator, and you should do this to save effort. You can do something like this:
That way, you get to reuse the hard work you did on implementing your == operator. This code exhibits fewer inconsistencies between == and !=, since we implemented one in terms of the other.
Also, if you are using the STL, it may be essential to define less-than (i.e., operator<). Operator less-than determines how sorting routines do their work.
You can express all other comparison operators in terms of operator== and operator< as shown below.
Pre-increment, pre-decrement, post-increment, and post-decrement operators are tricky not only because they must occur inside a class definition, but also because they are not intuitive. Like assignment, increment and decrement operators may only appear inside a class definition. C++ differentiates between pre and post versions of the operators by including a dummy integer argument.
The rationale for the dummy argument derives from implementation on the int class itself. Clearly a temporary storage is needed for the return value. Sadly, a dummy int argument does not suffice for most classes.
Sometimes programmers complain about the overhead of C++, and how this may be detrimental to embedded designs. Whereas there are a few features in C++ that can cause code bloat and slowdowns, this one definitely is not. Overloaded operators are simply a different way of invoking and naming ordinary function calls in a manner that is much easier to use. If programming embedded with C++, feel free to use overloaded operators as needs dictate.
A valid objection to overloaded operators comes when programmers use them unnaturally or in manners that obscure functionality. For example, overloading the division operator for classes modeling an engine does make much sense. What does it mean if I have engine1 and engine2 and express engine1 / engine2 ?
Also, operator precedence cannot be changed, so be careful when overloading && or || which also have aspects of shortcut behavior that need careful consideration.
In exchange, we will ask you to enter some personal details. To read about how we use your details, click here. On the registration form, you will be asked whether you want us to send you further information concerning other Doulos products and services in the subject area concerned.
Written by David C Black, Senior Member Technical Staff at Doulos. [Version 1.4.1]
This article is Copyright © 2018-2022 by Doulos. All rights are reserved.