Review - Operator Overloading

Thomas J. Kennedy

Contents:

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:

However, there are far more operators that we can mechanically overload, including:

There are a few more.. but we need only remember two things:

  1. Not all operators need to be defined for every ADT, nor does overloading a given operator necessarily make sense.

  2. 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:

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:

  1. It is an inline function in a header file. Since it is little more than a convenient wrapper for display… there is no need to place it in IntegerList.cpp. However, IntegerList::display's definition will be located inIntegerList.cpp`

  2. 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.

  3. 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 a friend 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: