Global training solutions for engineers creating the world's electronics

Introduction to C++ Operator Overloading

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
3
1
   float add(float lhs, float rhs);
2
   float multiply(float lhs, float rhs);
3
   void write(float& lhs, float rhs);

Then we would write something like:

xxxxxxxxxx
1
1
   write(RESULT, add(multiply(A,B),multiply(C,D)));

However, it is much easier to understand:

xxxxxxxxxx
1
1
   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
1
1
   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
3
1
  float  operator+ (const float& lhs, const float& rhs) const ; // add()
2
  float  operator* (const float& lhs, const float& rhs) const ; // multiply()
3
  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
1
1
  operator=(RESULT, operator+(operator*(A,B),operator*(C,D)));

This article further discusses some of the subtleties of operator overloading.

Overloadable Operators

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
5
1
   +   -    *    /    %    ^    &    |
2
   -   !    ,    =    <    >    <=   >=
3
   ++  --   <<   >>   ==   !=   &&   ||
4
   +=  -=   *=   /=   %=   ^=   &=   |=
5
   <<= >>=  []   ()   ->   ->*  new  delete

Additionally, there is a restriction that some operators are only overloadable in the context of a class. These are:

xxxxxxxxxx
1
1
[]   ()   ->   ->*   =   +=   -=   *=   /=   %=   ^=   &=   |=   <<=    >>=

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
1
1
    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?

      • Perhaps, if the images have origins and "merging" or "overlaying" is natural; however, it may not be intuitive.
    • Does the division of two shapes make sense?

      • Unlikely
    • What does it mean to apply less-than between two ethernet packets?

      • Probably not sensible.
  • 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.

Guidelines

Copy Assignment Operator

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:

  • My_class& operator=( const My_class& rhs );
  • My_class& operator=( My_class& rhs );
  • My_class& operator=( My_class rhs );
  • const My_class& operator=( const My_class& rhs );
  • const My_class& operator=( My_class& rhs );
  • const My_class& operator=( My_class rhs );
  • My_class operator=( const My_class& rhs );
  • My_class operator=( My_class& rhs );
  • My_class operator=( My_class rhs );

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
2
1
    My_class c1;
2
    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
2
1
    int a, b, c, d, e;
2
    a = b = c = d = e = 42;

The compiler interprets this code as:

xxxxxxxxxx
1
1
    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:

  • e = 42 assigns 42 to e, then returns e as the result
  • The value of e gets assigned to d, and then d is returned as the result
  • The value of d gets assigned to c, and then c is returned as the result
  • etc.

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
3
1
    My_class a, b, c;
2
    ...
3
    (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
6
1
    // Take a const-reference to the right-hand side of the assignment.
2
    // Return a non-const reference to the left-hand side.
3
    My_class& My_class::operator=(const My_class &rhs) {
4
      ...  // Do the assignment operation!
5
      return *this;  // Return a reference to myself.
6
    }

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
6
1
    My_class& My_class::operator=(const My_class &rhs) {
2
      // 1.  Deallocate any memory that My_class is using internally
3
      // 2.  Allocate some memory to hold the contents of rhs
4
      // 3.  Copy the values from rhs into this instance
5
      // 4.  Return *this
6
    }

Now, what happens when you do something like this:

xxxxxxxxxx
3
1
    My_class my_object;
2
    ...
3
    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
7
1
    My_class& My_class::operator=(const My_class &rhs) {
2
      // Check for self-assignment!
3
      if (this == &rhs)      // Same object?
4
        return *this;        // Yes, so skip assignment, and just return *this.
5
      ... // Deallocate, allocate new space, copy values...
6
      return *this;
7
    }

Or, you can simplify this a bit by doing:

xxxxxxxxxx
8
1
    My_class& My_class::operator=(const My_class &rhs) {
2
3
      // Only do assignment if RHS is a different object from this.
4
      if (this != &rhs) {
5
        ... // Deallocate, allocate new space, copy values...
6
      }
7
      return *this;
8
    }

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:

  • Take a const-reference for the argument (the right-hand side of the assignment).
  • Return a reference to the left-hand side, to support safe and reasonable operator chaining. (Do this by returning *this.)
  • Check for self-assignment, by comparing the pointers (this to &rhs).

Move Assignment Operator

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
1
1
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
8
1
    My_class& My_class::operator=( My_class&& rhs ) {
2
3
      // Only do assignment if RHS is a different object from this.
4
      if (this != &rhs) {
5
        ... // Deallocate, allocate new space, copy values... and make RHS invalid
6
      }
7
      return *this;
8
    }

Compound Assignment Operators

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
3
1
    My_class a, b;
2
    ...
3
    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
3
1
    My_class & My_class::operator+=(const My_class &rhs) {
2
      ...
3
    }

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
3
1
    My_class my_object;
2
    ...
3
    (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
4
1
    My_class& My_class::operator+=(const My_class &rhs) {
2
      ...   // Do the compound assignment work.
3
      return *this;
4
    }

As one last note, in general, you should beware of self-assignment with compound assignment operators as well.

Binary Arithmetic Operators +, -, *, and others

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
6
1
    // Construct rhs by value to use as a return value, then
2
    // add this instance's value to rhs and return rhs.
3
    My_class My_class::operator+(My_class rhs) const {
4
      rhs += *this; // Use += to add rhs to the copy.
5
      return rhs;   // All done!
6
    }

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
5
1
    // Add this instance's value to rhs and return a new instance
2
    // with the result.
3
    My_class My_class::operator+(const My_class &rhs) const {
4
      return My_class(*this) += rhs;
5
    }

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
3
1
    My_class a, b, c;
2
    ...
3
    (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:

  • Implement the compound assignment operators from scratch, and then define the binary arithmetic operators in terms of the corresponding compound assignment operators.
  • Return a const instance to prevent worthless and confusing assignment operations that shouldn't be allowed.

Comparison Operators

The comparison operators are straightforward. Define == first, using a function signature like this:

xxxxxxxxxx
3
1
    bool My_class::operator==(const My_class &rhs) const {
2
      ...  // Compare the values and return a bool result.
3
    }

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
3
1
    bool My_class::operator!=(const My_class &rhs) const {
2
      return !(*this == rhs);
3
    }

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
3
1
    bool My_class::operator<(const My_class &rhs) const {
2
      ... // Compare the values and return a bool result indicating "less-than" as it fits
3
    }

You can express all other comparison operators in terms of operator== and operator< as shown below.

xxxxxxxxxx
9
1
    bool My_class::operator<=(const My_class &rhs) const {
2
      return (*this == rhs) || (*this < rhs);
3
    }
4
    bool My_class::operator>=(const My_class &rhs) const {
5
      return !(*this < rhs);
6
    }
7
    bool My_class::operator>(const My_class &rhs) const {
8
      return !(*this <= rhs);
9
    }

Pre- and Post- increment/decrement Operators

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
9
1
    My_class& My_class::operator++(void) { // Pre-increment
2
      ++m_value;
3
      return *this;
4
    }
5
    My_class& My_class::operator++(int dummy) { // Post-increment
6
      My_class temp = *this;
7
      ++m_value;
8
      return temp;
9
    }

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.

Objections to Overloaded Operators

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.

A Complete Example

We now provide a complete example, which should help to complete your education on this topic. Inline definitions simplify the code presentation.

xxxxxxxxxx
91
1
static_assert( __cplusplus >= 201402L, "C++14 required" );< // Due to use of std::exchange
2
#include <iostream>
3
#include <utility>
4
#include <cmath>
5
6
struct Coord // 2-dimensional Cartesian coordinates
7
{
8
  // Constructors
9
  Coord( void ) = default;
10
  Coord( double x, double y ): m_x(x), m_y(y) {}
11
  // Because we are illustrating assignment, we need to obey the rule of 5
12
  Coord( const Coord& rhs ) : m_x{ rhs.m_x }, m_y{ rhs.m_y } {}
13
  Coord( Coord&& rhs ) noexcept
14
  : m_x{ std::exchange(rhs.m_x,std::nan("")) }
15
  , m_y{ std::exchange(rhs.m_y,std::nan("")) }
16
  {}
17
  // Accessors
18
  double& x( void ) { return m_x; }
19
  double& y( void ) { return m_y; }
20
  // Copy assignment
21
  Coord& operator=( const Coord& rhs ) {
22
    if( this != &rhs ) {
23
      m_x = rhs.m_x;
24
      m_y = rhs.m_y;
25
    }
26
    return *this;
27
  }
28
  // Move assignment
29
  Coord& operator=( Coord&& rhs ) {
30
    if( this != &rhs ) {
31
      m_x = std::exchange(rhs.m_x,std::nan(""));
32
      m_y = std::exchange(rhs.m_y,std::nan(""));
33
    }
34
    return *this;
35
  }
36
  // Arithmetic
37
  Coord& operator+=( const Coord& rhs ) {
38
    m_x += rhs.m_x;
39
    m_y += rhs.m_y;
40
    return *this;
41
  }
42
  Coord  operator+( const Coord& rhs ) const { return Coord{*this} += rhs; }
43
  Coord  operator+( void ) const { return *this; } // Unary +
44
  Coord  operator-( void ) const { return Coord{-m_x, -m_y }; } // Unary -
45
  Coord& operator-=( const Coord& rhs ) {
46
    *this += -rhs;
47
    return *this;
48
  }
49
  Coord  operator-( const Coord& rhs ) const { return Coord{*this} -= rhs; } // Subtract
50
  Coord& operator*=( double rhs ) {
51
    m_x *= rhs;
52
    m_y *= rhs;
53
    return *this;
54
  }
55
  Coord  operator*( double rhs ) const { return Coord{*this} *= rhs; }
56
  friend Coord  operator*( double lhs, const Coord& rhs ) {
57
    return rhs * lhs;
58
  }
59
  Coord& operator/=( double rhs ) {
60
    m_x /= rhs;
61
    m_y /= rhs;
62
    return *this;
63
  }
64
  Coord  operator/( double rhs ) const { return Coord{*this} /= rhs; }
65
  // Comparison
66
  bool operator==( const Coord& rhs ) const {
67
    return m_x == rhs.m_x && m_y == rhs.m_y; 
68
  }
69
  bool operator!=( const Coord &rhs ) const { 
70
    return !(*this == rhs); }
71
  bool operator< ( const Coord& rhs ) const { 
72
    return (rhs.m_x - m_x) < (m_y - rhs.m_y); 
73
  }
74
  bool operator<=( const Coord &rhs ) const { 
75
    return (*this == rhs) || (*this < rhs); 
76
  }
77
  bool operator>=( const Coord &rhs ) const { 
78
    return !(*this < rhs); 
79
  }
80
  bool operator> ( const Coord &rhs ) const { 
81
    return !(*this <= rhs); 
82
  }
83
  Coord& operator++() { m_x += 1.0; m_y += 1.0; return *this; }
84
  Coord& operator++(int) { auto previous{*this}; m_x += 1.0; m_y += 1.0; return previous; }
85
  friend std::ostream& operator<<( std::ostream& os, const Coord& rhs ) {
86
    os << "Coord{ " << rhs.m_x << ", " << rhs.m_y << " }";
87
    return os;
88
  }
89
private:
90
  double m_x, m_y;
91
};

You can find code for this example slightly expanded at https://github.com/Doulos/cpp_casting with some tests.

References

The end

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.