Section 5 - Overloading Insertion  and Assignment Ops

5A.5.1 The Insertion Operator

The insertion operator, <<, is used to send out primitive data types and even some built-in C++ class types.  We can "cout" things like ints, floats, CStrings and s-c strings.  But what about user-defined types?  If we overload the insertion operator, no problem.

The operator is used like so:

   cout << x;

Where cout is a pre-defined object of the C++ ostream class, and x is a type like int, float or string.   So we are looking at a binary operator that takes two parameters:

By the way, the fact that we have been doing this for weeks now, tells us that the insertion operator is already overloaded for us.  It must be overloaded for operator<<(ostream&, int), operator<<(ostream&, double), operator<<(ostream&, string), operator<<(ostream&, char *),  etc.  Somewhere deep in the catacombs of the  C++ library development edifice, a well paid compiler developer has apparently written these overloads for our benefit.  Let's take a moment to honor him or her.

Now it's our turn to overload this operator so that it can take one of our user-defined types in place of x.

What you need to know is this:

  1. We overload this as a normal, non-member function.
  2. The signature of the function is
    ostream & operator<<(ostream & ostrm, const OURCLASS & x);
    
  3. We normally declare this operator function a friend of the class to which we want it to apply

Let's see an example.  Say we have a class called Course that holds information about our university's courses.  We want to be able to print out Course objects using the insertion operator.  We first declare the operator method as a friend:

// Course prototype --------------------------------
class Course
{
  friend
  ostream & operator<<(ostream & ostrm, const Course & crs);

protected:
  string instructor;
  string title;

public:
  // other member functions
};

Then, later on, when we are ready to define the behavior of the << operator for Course objects, we do it like so:

// overloaded operator functions   ---------------------
ostream & operator<<(ostream & ostrm, const Course & crs)
{
  ostrm << "\n--------------------------------\n";
  ostrm << "Class Title: " << crs.title << "      "
  << "Instructor: " << crs.instructor << endl;
  ostrm << endl;
  return ostrm;
}

Finally, in main(), we just use it!

// main method ---------------------------------------
int main()
{
  Course phy27a("Harmon", "Physics");

  cout << phy27a;
}

As you can see, the definition is straightforward.  You send your data to the ostream parameter just as if it were cout.  You can call that formal parameter anything you like (but I call ostrm).  Normally the argument sent to it will be cout, but it could be a file stream as we will see when we get to files.

One detail is the return type and the return value.  Essentially, we want to return the same ostream object that we came in with. If the operator was invoked with cout, then we return cout.  The reason for this is we want to be able to use this in a chain of calls:

cout << "My course is  " << phy27a << endl;

That mechanism allows us to do so.  Why?  Remember that in the client, a functional return replaces the function call after the function is done.  Calling the operators from left to right in the sequence above, the first << to be called is highlighted:

cout << "My course is  " << phy27a << endl;

When the operation is complete (and "My course is " is sent to the screen), the highlighted operator call gets replaced with cout because the C++ library correctly returns it when it sends out primitive types and strings.  Perfect:

cout << phy27a << endl;

Next, the operator is called with the phy27a:

cout << phy27a << endl;

and the cout is returned again, because we were good little programmers and overloaded << for our Course object correctly, returning the ostream&, namely cout.  So we are left with:

cout << endl;

And you know how that ends -- with a line feed, carriage return.

The  moral here is that we must have a return statement at the end of our overloaded function definition that returns the first formal parameter (ostrm) that we were sent.

Again, this operator cannot be a member method.  You might be able to guess why, but if not, I'll tell you.  If it were a member method, the first parameter would be the object that did the calling, and since that is cout (usually) it would be a member of the ostream class (the left operand).  Well we don't have access to the definition of the ostream class.  So it has to be an ordinary method.

5A.5.2 The Assignment Operator

scenic cactiOverloading the assignment operator is very much like defining a copy constructor.  We are trying to allow the client to write lines like:

cs2a = phy15a;

where the objects on each side are from our user-defined class.  Normally we write assignment statements like the one above without taking any extra measures, but if our classes have external heap memory (i.e., deep memory) that they manage, we know that this won't do (for the same reason that copy constructors were needed: we don't want pointers inside two different objects pointing to the same heap data).  Therefore, we often need to define the assignment operator explicitly for our classes.

Here is a simple example for the above Course class.  Since there is no heap data, this example is a little too simple;  we don't strictly have to have it.  But if there were heap data around, we would allocate and copy that inside the definition of the operator function.

Course & Course::operator=( const Course &crs)
{
  if (this != &crs)
  {
    this->instructor = crs.instructor;
    this->title = crs.title;
  }
  return *this;
}

Notice the return value, *this. That is the object that called us.  We are evidently returning the object to which we just made the assignment.  Why?

The answer is similar to the return of the ostream& parameter in the insertion operator, explained above, but in reverse.  In a string of assignments:

cs2a = cs2b = cs2c = courseTemplate;

We want all the courses on the left to get the value of the courseTemplate on the far right.  The assignment operator evaluates from right to left, opposite of the insertion operator.  So the first operator to be executed is this one:

cs2a = cs2b = cs2c = courseTemplate;

That operator=() function call is replaced with cs2c (the new value after assignment, which is a copy of courseTemplate) if we have written our operator to return *this, as we should.  This repeats itself, so that the courses_template values are passed down the chain of = operators until they are all correctly assigned.

Another detail is the test if (this != &crs).  What are we testing and why are we testing it?