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:
xxxxxxxxxx
float add(float lhs, float rhs);
float multiply(float lhs, float rhs);
void write(float& lhs, float rhs);
Then we would write something like:
xxxxxxxxxx
write(RESULT, add(multiply(A,B),multiply(C,D)));
However, it is much easier to understand:
xxxxxxxxxx
RESULT = A * B + C * D ;
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:
xxxxxxxxxx
RESULT = LHS + RHS ;
Thus the C++ compiler automatically parses the above and then calls functions. The names of the functions are a bit odd but easily understandable:
xxxxxxxxxx
float operator+ (const float& lhs, const float& rhs) const ; // add()
float operator* (const float& lhs, const float& rhs) const ; // multiply()
float& operator= (const float& lhs, const float& rhs); // write()
It is technically legal to write the following hard to understand code that is effectively just like the initially proposed solution:
xxxxxxxxxx
operator=(RESULT, operator+(operator*(A,B),operator*(C,D)));
This article further discusses some of the subtleties of operator overloading.
All C++ operators can be overloaded except for the following:
Descriptive name | Operator |
---|---|
Member access operator | x.y |
Dereferencing pointer to member | x.*y |
Scope resolution operator | x::y |
Ternary condition operator | x?y:z |
Byte sizing operator | sizeof(x) |
Member access operator | object.field |
Pointer to member access | object.*field |
Runtime type identification | typeid(e) |
Add/remove const property | const_cast<T>(e) |
Compile-time cast | static_cast<T>(e) |
Run-time cast | dynamic_cast<T>(ptr) |
Reinterpret cast | reinterpret_cast<T>(e) |
Those that can be overloaded are:
xxxxxxxxxx
+ - * / % ^ & |
- ! , = < > <= >=
++ -- << >> == != && ||
+= -= *= /= %= ^= &= |=
<<= >>= [] () -> ->* new delete
Additionally, there is a restriction that some operators are only overloadable in the context of a class. These are:
xxxxxxxxxx
[] () -> ->* = += -= *= /= %= ^= &= |= <<= >>=
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:
xxxxxxxxxx
a + b * c // a, b, c are some class instances
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).
C++ does not "understand" the meaning of an overloaded operator. It's the programmer's responsibility to provide meaningful overloaded functions. YOU, the programmer, should think carefully about how others might perceive your choice as being a "natural representation." Consider:
Does adding a video image to another image make intuitive sense?
Does the division of two shapes make sense?
What does it mean to apply less-than between two ethernet packets?
C++ cannot derive complex operators from simple ones. For instance, if you define overloaded operator functions operator* and operator=, C++ cannot evaluate the expression a *= b correctly. You have to do this with your additional overloads of the respective operators (e.g., *=).
Programs may never change the syntax of an overloaded operator. Binary operators must remain binary. Unary operators must remain unary. Some have two forms (e.g., Consider the meaning of - in y = a - b and y = - a).
Programmers cannot invent new operators for use in expressions. Only existing operators listed in the syntax of the language can be overloaded. However, the programmer can always write functions for individual cases.
The programmer may overload the operators ++ and -- in both prefix and suffix forms.
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:
xxxxxxxxxx
My_class c1;
c1 = My_class( 5, 'a', "Hello World" ); // assume this constructor exists
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:
xxxxxxxxxx
int a, b, c, d, e;
a = b = c = d = e = 42;
The compiler interprets this code as:
xxxxxxxxxx
a = (b = (c = (d = (e = 42))));
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:
xxxxxxxxxx
My_class a, b, c;
...
(a = b) = c; // What??
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 `int's, it's good enough for user-defined datatypes."
So, for the hypothetical My_class assignment operator, do something like this:
xxxxxxxxxx
// Take a const-reference to the right-hand side of the assignment.
// Return a non-const reference to the left-hand side.
My_class& My_class::operator=(const My_class &rhs) {
... // Do the assignment operation!
return *this; // Return a reference to myself.
}
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:
xxxxxxxxxx
My_class& My_class::operator=(const My_class &rhs) {
// 1. Deallocate any memory that My_class is using internally
// 2. Allocate some memory to hold the contents of rhs
// 3. Copy the values from rhs into this instance
// 4. Return *this
}
Now, what happens when you do something like this:
xxxxxxxxxx
My_class my_object;
...
my_object = my_object; // BOOM!
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:
xxxxxxxxxx
My_class& My_class::operator=(const My_class &rhs) {
// Check for self-assignment!
if (this == &rhs) // Same object?
return *this; // Yes, so skip assignment, and just return *this.
... // Deallocate, allocate new space, copy values...
return *this;
}
Or, you can simplify this a bit by doing:
xxxxxxxxxx
My_class& My_class::operator=(const My_class &rhs) {
// Only do assignment if RHS is a different object from this.
if (this != &rhs) {
... // Deallocate, allocate new space, copy values...
}
return *this;
}
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.
xxxxxxxxxx
My_class& operator=( My_class&& rhs );
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.
xxxxxxxxxx
My_class& My_class::operator=( My_class&& rhs ) {
// Only do assignment if RHS is a different object from this.
if (this != &rhs) {
... // Deallocate, allocate new space, copy values... and make RHS invalid
}
return *this;
}
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:
xxxxxxxxxx
My_class a, b;
...
a += b; // Same as a.operator+=(b)
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:
xxxxxxxxxx
My_class & My_class::operator+=(const My_class &rhs) {
...
}
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:
xxxxxxxxxx
My_class my_object;
...
(my_object += 5) += 3;
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:
xxxxxxxxxx
My_class& My_class::operator+=(const My_class &rhs) {
... // Do the compound assignment work.
return *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:
xxxxxxxxxx
// Construct rhs by value to use as a return value, then
// add this instance's value to rhs and return rhs.
My_class My_class::operator+(My_class rhs) const {
rhs += *this; // Use += to add rhs to the copy.
return rhs; // All done!
}
Simple!
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:
xxxxxxxxxx
// Add this instance's value to rhs and return a new instance
// with the result.
My_class My_class::operator+(const My_class &rhs) const {
return My_class(*this) += rhs;
}
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:
xxxxxxxxxx
My_class a, b, c;
...
(a + b) = c; // Hmm...
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:
xxxxxxxxxx
bool My_class::operator==(const My_class &rhs) const {
... // Compare the values and return a bool result.
}
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:
xxxxxxxxxx
bool My_class::operator!=(const My_class &rhs) const {
return !(*this == rhs);
}
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.
xxxxxxxxxx
bool My_class::operator<(const My_class &rhs) const {
... // Compare the values and return a bool result indicating "less-than" as it fits
}
You can express all other comparison operators in terms of operator== and operator< as shown below.
xxxxxxxxxx
bool My_class::operator<=(const My_class &rhs) const {
return (*this == rhs) || (*this < rhs);
}
bool My_class::operator>=(const My_class &rhs) const {
return !(*this < rhs);
}
bool My_class::operator>(const My_class &rhs) const {
return !(*this <= rhs);
}
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.
xxxxxxxxxx
My_class& My_class::operator++(void) { // Pre-increment
++m_value;
return *this;
}
My_class& My_class::operator++(int dummy) { // Post-increment
My_class temp = *this;
++m_value;
return temp;
}
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.
We now provide a complete example, which should help to complete your education on this topic. Inline definitions simplify the code presentation.
xxxxxxxxxx
static_assert( __cplusplus >= 201402L, "C++14 required" );< // Due to use of std::exchange
struct Coord // 2-dimensional Cartesian coordinates
{
// Constructors
Coord( void ) = default;
Coord( double x, double y ): m_x(x), m_y(y) {}
// Because we are illustrating assignment, we need to obey the rule of 5
Coord( const Coord& rhs ) : m_x{ rhs.m_x }, m_y{ rhs.m_y } {}
Coord( Coord&& rhs ) noexcept
: m_x{ std::exchange(rhs.m_x,std::nan("")) }
, m_y{ std::exchange(rhs.m_y,std::nan("")) }
{}
// Accessors
double& x( void ) { return m_x; }
double& y( void ) { return m_y; }
// Copy assignment
Coord& operator=( const Coord& rhs ) {
if( this != &rhs ) {
m_x = rhs.m_x;
m_y = rhs.m_y;
}
return *this;
}
// Move assignment
Coord& operator=( Coord&& rhs ) {
if( this != &rhs ) {
m_x = std::exchange(rhs.m_x,std::nan(""));
m_y = std::exchange(rhs.m_y,std::nan(""));
}
return *this;
}
// Arithmetic
Coord& operator+=( const Coord& rhs ) {
m_x += rhs.m_x;
m_y += rhs.m_y;
return *this;
}
Coord operator+( const Coord& rhs ) const { return Coord{*this} += rhs; }
Coord operator+( void ) const { return *this; } // Unary +
Coord operator-( void ) const { return Coord{-m_x, -m_y }; } // Unary -
Coord& operator-=( const Coord& rhs ) {
*this += -rhs;
return *this;
}
Coord operator-( const Coord& rhs ) const { return Coord{*this} -= rhs; } // Subtract
Coord& operator*=( double rhs ) {
m_x *= rhs;
m_y *= rhs;
return *this;
}
Coord operator*( double rhs ) const { return Coord{*this} *= rhs; }
friend Coord operator*( double lhs, const Coord& rhs ) {
return rhs * lhs;
}
Coord& operator/=( double rhs ) {
m_x /= rhs;
m_y /= rhs;
return *this;
}
Coord operator/( double rhs ) const { return Coord{*this} /= rhs; }
// Comparison
bool operator==( const Coord& rhs ) const {
return m_x == rhs.m_x && m_y == rhs.m_y;
}
bool operator!=( const Coord &rhs ) const {
return !(*this == rhs); }
bool operator< ( const Coord& rhs ) const {
return (rhs.m_x - m_x) < (m_y - rhs.m_y);
}
bool operator<=( const Coord &rhs ) const {
return (*this == rhs) || (*this < rhs);
}
bool operator>=( const Coord &rhs ) const {
return !(*this < rhs);
}
bool operator> ( const Coord &rhs ) const {
return !(*this <= rhs);
}
Coord& operator++() { m_x += 1.0; m_y += 1.0; return *this; }
Coord& operator++(int) { auto previous{*this}; m_x += 1.0; m_y += 1.0; return previous; }
friend std::ostream& operator<<( std::ostream& os, const Coord& rhs ) {
os << "Coord{ " << rhs.m_x << ", " << rhs.m_y << " }";
return os;
}
private:
double m_x, m_y;
};
You can find code for this example slightly expanded at https://github.com/Doulos/cpp_casting with some tests.
Written by David C Black, Senior Member Technical Staff at Doulos. Version 1.4.1
This article is Copyright (C) 2018-2024 by Doulos. All rights are reserved.