Review - Operator Overloading
Thomas J. Kennedy
1 The Big Picture
Operator overloading refers to the process of defining what an operator means for a given ADT (in C++, a class
or struct
). If you look ahead to the C++ class checklist… you will find that we usually focus on a few operators:
- Assignment operator (
operator=
) as part of the Big-3 - Logical Equivalence operator (
operator==
) - “Less than” or “comes before” operator (
operator<
) - Stream insertion or “output” or “
cout
operator” (operator<<
)
However, there are far more operators that we can mechanically overload, including:
-
Comparison operators
operator<
operator<=
operator==
operator!=
operator>
operator>=
- C++20 adds the spaceship operator (
operator<>
)!
-
Arithmetic operators
operator+
operator+=
operator-
operator-=
operator*
operator*=
operator/
operator/=
-
Memory allocation
operator new
There are a few more.. but we need only remember two things:
-
Not all operators need to be defined for every ADT, nor does overloading a given operator necessarily make sense.
-
For a few operators (namely the logical equivalence operators), the compiler can build a few for us (e.g., using
std::rel_ops
).
2 An Example Class
Let us suppose that we are defining a new ADT using a class… an IntegerList
ADT.
Example 1: IntegerList Header File (`IntegerList.h`)/** * A container class for storing, accessing, and retrieving a sequence of * integers. */ class IntegerList { private: /** * The internal, encapsulated, data structure where numbers are * actually stored. */ std::vector<int> theNumbers; public: /** * This is the default constructor. Set up an empty list. */ IntegerList(); /** * This is the copy constructor. Set up a duplicate copy of an existing * list. * * @param src existing IntegerList object to copy */ IntegerList(const IntegerList& src); /** * The Destructor can be generated automatically by the compiler in * this case. However, modern best practice all-but-requires us to * explicitly state our intentions. * * This only works when we know that we will not be working with * pointers. */ ~IntegerList() = default; /** * Add an integer to the list. * * @param aNewInt integer to add */ void add(int aNewInt); /** * Compare two IntegerList objects for equivalance. * * @param rhs the righ-hand-side (lhs < rhs) IntegerList * * @returns true if the two IntegerLists contain the same integers in * the same order. */ bool operator==(const IntegerList& rhs) const; /** * This is a display helper function that will be called by the stream * insertion operator. * * @param outs output desitation (e.g., `cout`) */ void display(std::ostream& outs) const; } /** * Overloaded stream insertion operator. Output each number one per line. * * @param toPrint the IntegerList to print/display */ inline std::ostream operator<<(std::ostream& outs, const IntegerList& toPrint) { toPrint.display(outs); return outs; }
Note that this class is not complete. It is missing a few items from the C++ Class Checklist and general class checklist. For now we will focus on two operators:
operator==
operator<<
2.1 The Stream Insertion Operator
Let us start with the stream insertion operator (operator<<
).
Example 2: Stream Insertion Operator/** * Overloaded stream insertion operator. Output each number one per line. * * @param toPrint the IntegerList to print/display */ inline std::ostream operator<<(std::ostream& outs, const IntegerList& toPrint) { toPrint.display(outs); return outs; }
Take note of a few things:
-
It is an
inline
function in a header file. Since it is little more than a convenient wrapper fordisplay
… there is no need to place it inIntegerList.cpp
. However, IntegerList::display's definition will be located in
IntegerList.cpp` -
const
correctness -toPrint
is passed by constant reference. This allows direct read-only access to the IntegerList object and guarantees that no changes are made to the list. -
operator<<
is not implemented as a member function. This operator can only be implemented as a wrapper function (as shown in this example) or as afriend
function. In general…friend
functions should be avoided.
Example 3: display Definition (from IntegerList.cpp)//------------------------------------------------------------------------------ void IntegerList::display(std::ostream& outs) const { const int numInts = theNumbers.size(); for (int i = 0; i < numInts; i++) { outs << theNumbers[i] << "\n"; } }
Notice the trailing const
after the definition. This “first” const
forces this member function to be read-only. It can access the list, but not change any data (e.g., add or remove numbers). How surprised would you be if you output a list and a few numbers disappeared?
2.2 The Logical Equivalence Operator
Example 4: operator== Definition (from IntegerList.cpp)//------------------------------------------------------------------------------ void IntegerList::operator==(const IntegerList& rhs) const { // If the two lists are not of the same length, they are guaranteed not to // contain the same numbers.. if (this->theNumbers.size() != rhs.theNumbers.size()) { return false; } // Use a reference variable for convenience and readability const IntegerList& lhs = *this; const int numInts = lhs.theNumbers.size(); for (int i = 0; i < numInts; i++) { if (lhs.theNumbers[i] != rhs.theNumbers[i]) { return false; } } // If this statement is reached, the two lists are identical. return true; }
Note how much more fun this operator is to implement! Take note of a few things:
-
const
correctness - boththis
andrhs
can only be read. They can not be changed. -
const IntegerList& lhs = *this;
- This line is quite convenient.-
Reference variables allow more convenient names (and access) to existing variables. In fact… these are what pass-by-reference uses!
-
this
is the self pointer. It is always a pointer to the current object. Note that other languages (namely Python and Rust) useself
instead (which is a much better choice… in my opinion).
-