NOTE:

These are a little sketchy...I'll fill in details 'soon'.

Why Overload operators?

To make our classes behave more naturally, of course! Aren't you sick of the whole object.method() syntax to make class objects behave a certain way? It is tedious at best!

So let's overload an appropriate operator to represent certain behaviors and then our objects can behave as naturally as the built-in types!

Take output or input for instance. We would normally have to code crap like this:

    cout << "Please enter data:  ";
    object.input();

    cout << "You entered ";
    object.output();
    cout << ".\n";

But with overloaded operators we could use the more natural pattern:

    cout << "Please enter data:  ";
    cin >> object;

    cout << "You entered " << object << ".\n";

Wouldn't that be sweet?!

All C++ operators

Just so you know what you are dealing with, here is a list of operator precedence and associativity.

There is actually an even more detailed treatment of operator precedence and overloadability on wikipedia.

Overloading Rules

Thou shalt not:

Thou shalt always:

Thou should always:

Overloading Patterns

Unary operators

The unary operators are: +, -, ~, !, *, &, and ->. (-> is actually a binary operator, but it overloads as a unary operator! More on that later...)

Example calls:

   OOver x, y;
   y = -x;       // y is arithmetic negation of x

   y = -x / 2;   // y is half of arith neg of x

   x = -x;       // arithmetically negate x
As a MethodAs a Non-Member
In Class:
   class OOver
   {
      OOver operator-(void) const;
   };

Definition:
   OOver OOver::operator-(void) const
   {
      OOver t(*this);
      // arithmetically negate t
      return t;
   }

Examples as Operator Function Calls:
   y.operator=(x.operator-());
   y.operator=(x.operator-().operator/(2));
   x.operator=(x.operator-());
Friendship (if a friend):
   class OOver
   {
      friend OOver operator-(const OOver & o);
   };

Definition:
   OOver operator-(const OOver & o)
   {
      OOver t(o);
      // arithmetically negate t
      return t;
   }

Examples as Operator Function Calls:
   y.operator=(operator-(x));
   y.operator=(operator/(operator-(x), 2));
   x.operator=(operator-(x));

(Note that you do not have to declare friendship with non-member operator forms. friendship just makes the coding easier since you don't have to go through accessors to use the private data members. Some people would even rail against you for using friendship — so be warned!)

Binary operators

The binary operators are:

+, -, *, /, %
&, ¦, ^, <<, >>
=, +=, -=, *=, /=, %=, &=, ¦=, ^=, <<=, >>=
[], ()
&&, ¦¦, <, <=, >, >=, ==, !=
,

Example calls:

   OOver x, y, z;
   y = z+x;         // y sum of z and x

   y = (z+x) / 2;   // y is half of sum of z and x

   z = z+x;         // increment z by x
As a MethodAs a Non-Member
In Class:
   class OOver
   {
      OOver operator+(const OOver & p) const;
   };

Definition:
   OOver OOver::operator+(const OOver & p) const
   {
      OOver t(*this);
      // add p to t
      return t;
   }

Examples as Operator Function Calls:
   y.operator=(z.operator+(x));
   y.operator=(z.operator+(x).operator/(2));
   z.operator=(z.operator+(x));
Friendship (if a friend):
   class OOver
   {
      friend OOver operator+(const OOver & o,
                             const OOver & p);
   };

Definition:
   OOver operator+(const OOver & o,
                   const OOver & p)
   {
      OOver t(o);
      // add p to t
      return t;
   }

Examples as Operator Function Calls:
   y.operator=(operator+(z, x));
   y.operator=(operator/(operator+(z, x), 2));
   z.operator=(operator+(z, x));

(Note that you do not have to declare friendship with non-member operator forms. friendship just makes the coding easier since you don't have to go through accessors to use the private data members. Some people would even rail against you for using friendship — so be warned!)

operator= is Special

But let's not forget the special pattern for operator=. (It only has to be used for classes with dynamically allocated members, after all.)

    DMemb & DMemb::operator=(const DMemb & d)
    {
        if (this != &d)  // avoid self-assignment
        {
            delete dyn;         // must delete (existing) dynamic member
                                // before creating new space!

            set_dyn(d.dyn);     // allocate and initialize dynamic members

            set_stat(d.stat);   // don't forget non-dynamic members
        }
        return *this;   // return reference to caller
    }

Funny Operators

Compatibility with the Built-In Types

If your target type (class, enum, etc.) needs to be compatible with other types already present (short, double, ostream, etc.), you will have to provide that compatibility in terms of multiple overloads. For example:

   class Rational
   {
      Rational operator+(const Rational & r) const;
      Rational operator+(long n) const;
   };

   Rational operator+(long n, const Rational & r);

   ostream & operator<<(ostream & out, const Rational & r);
   istream & operator>>(istream & in, Rational & r);

Here we have allowed Rational numbers to be directly added with not only each other, but also integers as well. The choice of long should allow the compiler to bring in any integer value. Some compilers may have trouble, however, and you may need further overloads to force things to work.

We've also overloaded insertion and extraction so that Rational numbers can be used in standard stream operations. Note how the return type of the stream operators is the same as the stream argument. This is because we want the streams to be 'stream-able' (aka chain-able or cascade-able). That is, we want to be able to print or read multiple things at once:

   Rational r1, r2, sum;
   long i;
   cout << "Enter two rational numbers and an integer:  ";
   cin >> r1 >> r2 >> i;
   sum = r1 + r2;
   cout << "This is your sum:  " << sum << ".\n";
   sum = r1 + i;
   cout << "              or:  " << sum << ".\n";
   sum = i + r2;
   cout << "              or:  " << sum << ".\n";

Efficiency

In the patterns above, we saw a local temporary variable being declared (copy constructed from either the first argument or the calling object), altered, and returned by value (necessitating a second copy construction for the return value).

This is a long process and can involve two or more constructions and often several mutator calls. Such code can run terribly slowly — especially if there is dynamic memory being used in the target type (class).

For efficiency's sake, we often replace code such as:

   Rational Rational::operator+(const Rational & r) const
   {
      Rational t(*this);
      t.set_num(t.num*r.den+t.den*r.num);
      t.set_den(t.den*r.den);
      return t;
   }

With a one-liner such as:

   Rational Rational::operator+(const Rational & r) const
   {
      return Rational(num*r.den+den*r.num, den*r.den);
   }

By constructing the return value on the return statement, we indicate to the compiler that it doesn't have to hold a local copy for any length of time. Most compilers can then create the return value directly with a single constructor call.

The first version called two constructors and two mutators. The last version calls one constructor and two mutators.

If this class had involved dynamic memory, there would have been at least four mutator calls in the first version (more likely 6). But there would still only be two mutator calls in the second version.

So, although there is not a lot saved in the Rational class, we still prefer the one-liner version for its compactness, elegance, and clear style. It will also prove easier and faster for you to type. (And since it is so compact and small, you'll likely make it an inline function, too!)

This technique of returning a temporary/anonymous class object construction is so important that we've named it the RVOreturn [by] value optimization. (But you already knew that, didn't you? *grin*)

Examples

Lecture example: stream operations.

Lecture example: increment and addition operations.

Lecture example: addition operations again.

Lecture example: addition operations also.

Lecture example: addition operations finale.

For the above examples of Rational (R) compatibility with long integers (l), here's a handy table of possibilities:

constructor member operator+ non-member operator+
normalR+R, R+ll+R
normalR+Rl+R
normalR+R
explicit R+R, R+ll+R

Note that the compiler will coerce for argument objects but not for calling objects. And, of course, with your constructor made explicit, no coercion path exists from built-in type to your class object at all. (And also, by a normal constructor here I mean non-explicit but with a single argument possible. It's the single-argument path that makes the coercion work in the first place, after all...)

Lecture example: subscript operation(s).

Little Discussed operator Issues

Here are a couple of issues I've noticed from journal articles, conference proceedings, UseNet discussions, and/or books, but haven't seen much of in text books — notably yours. *smile* (I will, of course, leave out of the discussion those details about language features we don't know or yet care about...namely thread and/or exception safety.)

+, =, and += (or the like...)

We know that a trio of operators like +, =, and += are distinct entities which must be individually overloaded for our own data types ...should we see the need. However, there seems to have arisen a question of how this is best to be done.

The style I learned (we'll call it the traditional style, for lack of a better name), was to overload the basic operators (here + and =) and use them to overload the combination or short-hand form (+=). For instance, if we were dealing with a class for handling the concept of Complex numbers, we might have:

    Complex & Complex::operator=(const Complex & r)
    {
        if (this != &r)
        {
            real = r.real;
            imag = r.imag;
        }
        return *this;
    }

    Complex Complex::operator+(const Complex & r) const
    {
        return Complex(real+r.real, imag+r.imag);
    }

    Complex & Complex::operator+=(const Complex & r)
    {
        return *this = *this + r;
    }

Notice that we allow the constructor call on the +'s return to both perform any required mutation for data validation and to allow for the 'constructor-return optimization' to avoid a separate constructor call for the Complex object returned by value.

Now let's look at a more recently popular style (terming this the recent style):

    Complex & Complex::operator=(const Complex & r)
    {
        if (this != &r)
        {
            real = r.real;
            imag = r.imag;
        }
        return *this;
    }

    Complex & Complex::operator+=(const Complex & r)
    {
        set_real(real + r.real);
        set_imag(imag + r.imag);
        return *this;
    }

    Complex Complex::operator+(const Complex & r) const
    {
        Complex temp(*this);
        //temp += r;
        return temp += r; // temp;
    }

Here we take an almost backwards approach by defining all of the assignment family of operators (= and +=) and then using the combination/short-hand one to define the arithmetic operation (+).

This forces the compiler to create a copy of the local object during the return from +. It also explicitly creates a copy of the calling object into the local temporary. Further, the += must perform all of its own mutation calls to avoid damaging the data with 'bad' values.

Which seems better to you? Hmm...

Built-In Type Inter-Compatibility

Also notice the subtle differences between explicitly overloading for built-in type compatibility:

    Complex Complex::operator+(double r) const
    {
        return Complex(real + r, imag);
    }

    Complex & Complex::operator=(double r)
    {
        set_real(r);
        set_imag(0.0);
        return *this;
    }

    Complex & Complex::operator+=(double r)
    {
        return *this = *this + r;
    }

versus using a type-conversion constructor implicitly:

    // separate from the initialization/parameterized/unclassified
    // constructor:
    //Complex::Complex(double r) : real(0.0), imag(0.0)
    //{
    //    set_real(r);
    //}
    //
    // or as a defaulted case for the init/param/unclassified ctor:
    Complex::Complex(double r, double i=0.0) : real(0.0), imag(0.0)
    {
        set_real(r);
        set_imag(i);
    }

The differences here are much more subtle and often lead to wild claims and/or accusations. Even when all functions are easily inlined as these are, there will be a bit more time during the compilation of the program with the type-conversion constructor approach.

The explicit overloading approach, on the other hand, takes more programmer time and can lead to accidental lack of protection by programmers who fail to call mutators in such situations.

operator()

Notes from lecture.

Overloading Is Not Just For classes!

It works, for instance, for enumerations.

However, it does not work for typedefs.

(It also works for structures and unions. But we often restrict ourselves to non-member operators to keep our C brethren sane(r).)