Operator Overloading Basics
Fundamentals
- There are many operators available that work on built-in types, like
int and double.
- Operator overloading -- is the creation of new versions
of these operators for use with user-defined types.
- It is not as difficult as it sounds. Some things to note:
-
An operator in C++ is just a function that is called with
special notation (usually more intuitive or familiar notation).
Overloading an operator simply involves writing a function.
- C++ already does some operator overloading implicitly on
built-in types. Consider the fact that the + operator already works for
ints, floats, doubles, and chars. There is really a different
version of the + operator for each type.
- Operator overloading is done for the purpose of using familiar
operator notation on programmer-defined types (classes).
Some rules regarding operator overloading
-
Overloading an operator cannot change its precedence.
-
Overloading an operator cannot change its associativity.
-
Overloading an operator cannot change its "arity" (i.e. number of operands)
-
It is not possible to create new operators -- only new versions of existing
ones.
-
Operator meaning on the built-in types cannot be changed.
friend functions vs. member functions
- Some operators can be written as member functions of a
class
- Some operators can be written as stand-alone functions -- it's common
to use friend on these
- Some operators can be written either way (your choice)
- A binary operator has two operands
- Written as a stand-alone function, both operands would be sent as
parameters, so it would be a function with two parameters
- Written as a member function, the first operand would be the
calling object, and the other would be sent as a parameter
(i.e. a function with one parameter)
- A unary operator has one operand
- As a stand-alone function, the operand is sent as a parameter
- As a member function, one calling object, no parameters
Format
Motivation
Example 1
- Consider the arithmetic operators. These are already familiar
notations from algebra and earlier.
int x = 3, y = 6, z;
float a = 3.4, b = 2.1, c;
z = x + y;
c = a / b;
- Now consider this notation. This should also be familiar from high
school or earlier:
1/2 + 1/3 // evaluates to 5/6
2/3 - 1/3 // evaluates to 1/3
- Now, what about using arithmetic operators on our Fraction
class (a user-defined type):
Fraction n1, n2, n3;
n3 = n1 + n2; // will the compiler accept this?
- It should be clear that this would not make sense to the
compiler. Fraction is a programmer-defined type. How would the
computer know about common denominators, etc?
- These code statements would be nice to use, however, because it's the
same way we use other numeric types (like int and
double).
- Operator overloading makes this possible
Example 2
- Consider screen output. We know that this is legal:
int x = 5, y = 0;
cout << x << y;
- But how about this?
Fraction f(3,4);
cout << "The fraction f = " << f << '\n';
- There is no insertion operator in the iostream library to
handle Fraction objects. How would the library designers know
what types we were going to build?
- But with operator overloading, we can define a new version of the
insertion operator << to make this work
Overloading the Arithmetic operators
The arithmetic operators can be overloaded either as stand-alone
functions or as member functions.
Overloading as a friend function
- To add objects, we could write a function called Add, of
course. Recall this example:
friend Fraction Add(Fraction f1, Fraction f2);
With this function prototype, a sample call would be:
Fraction n1, n2, n3;
n3 = Add(n1, n2);
- The + notation would certainly be more convenient. The operator
version just has a different name:
friend Fraction operator+(Fraction f1, Fraction f2);
The usual function style call would look like this:
n3 = operator+(n1, n2);
While this is legal, the advantage is being able to use the more common
infix notation:
n3 = n1 + n2; // this becomes legal
- Here's a full definition of the + operator for class
Fraction. Note that this is also a possible definition for the
Add function:
Fraction operator+(Fraction f1, Fraction f2)
{
Fraction r; // declare a Fraction to hold the result
// load result Fraction with sum of adjusted numerators
r.numerator = (f1.numerator*f2.denominator)
+ (f2.numerator*f1.denominator);
// load result with the common denominator
r.denominator = f1.denominator * f2.denominator;
return r; // return the result Fraction
}
- Once this operator overload is defined, then the following is
legal. Note that cascading also works (because the operation returns a
Fraction). Now we have the standard intuitive use of +
Fraction n1, n2, n3, n4, n5;
n5 = n1 + n2 + n3 + n4; // now it is legal!
- Note: This function could also be written with const reference
parameters
friend Fraction operator+(const Fraction& f1, const Fraction& f2);
- Click here to see the Fraction class
with operator+ added in (as a friend function)
Overloading an operator as a member function
- One member function version of Add was:
Fraction Add(const Fraction& f) const;
A sample call to this function:
Fraction n1, n2, n3;
n3 = n1.Add(n2);
- The corresponding operator overload. Again, we change the name to
operator+
Fraction operator+(const Fraction& f) const;
Again, we could use typical function notation, and the dot-operator:
n3 = n1.operator+(n2);
But the whole point is to be able to use the more familiar
notation, which still works, and no dot-operator required:
n3 = n1 + n2; // n1 is the calling object, n2 is the argument
- A full definition of this version of operator+
Fraction Fraction::operator+(const Fraction& f2) const
{
Fraction r; // result
r.numerator = (numerator * f2.denominator)
+ (f2.numerator * denominator);
r.denominator = (denominator * f2.denominator);
return r;
}
- Click here to see the Fraction class
with the + operator as a member function
Some other possible arithmetic operators
- Prototypes only
// multiplication overload for Fractions
friend Fraction operator*(Fraction f1, Fraction f2);
// addition operator to add a Fraction and an integer
friend Fraction operator+(Fraction f, int n);
// same as above, but this one allows the int to come first in the call
friend Fraction operator+(int n, Fraction f);
- Note that these last two are not really needed in the Fraction class,
since we have a conversion constructor! The normal operator+
function:
friend Fraction operator+(const Fraction& f1, const Fraction& f2);
will take care of calls that involve type conversions from int to
Fraction, via the conversion constructor:
Fraction n1, n2, n3;
n3 = n1 + 5;
n3 = 10 + n2;
However, ask yourself... Will both of these work for the friend version
and the member function version? Why or why not?
Overloading comparison operators
- The comparison operators can also be overloaded either as stand-alone
functions or member functions
- Consider the Equals function example:
friend bool Equals(const Fraction& f1, const Fraction& f2);
We can easily write this as an operator overload:
friend bool operator== (const Fraction& f1, const Fraction& f2);
- Here are corresponding sample calls:
Fraction n1, n2;
if (Equals(n1, n2))
cout << "n1 and n2 are equal";
Contrast with this:
Fraction n1, n2;
if (n1 == n2)
cout << "n1 and n2 are equal";
- To get a full set of comparison operations, overload all 6 of them:
- operator ==
- operator !=
- operator <
- operator >
- operator <=
- operator >=
Overloading the insertion << and extraction >> operators:
As with other operators, the << and >> operators are defined for
the basic types. If you build your own class, don't expect <<
to automatically work with your new types of objects! If you
want it to work, you have to teach the computer how to do such output.
Consider the following:
Fraction f;
cout << f; // how would the machine know how to do this?
We have no reason to expect the second line to work! The insertion
operator << is only pre-defined for built-in types. The iostream.h
library doesn't know about the Fraction type.
The << operator is a binary operator (2 parameters, left side
and right side). The first parameter is always an ostream
object (we've mostly used cout, so far). Because of this, it cannot
be defined as a member function (it would have to be a member of the ostream
class, which we cannot change). The << and >> operators should
always be defined as outside functions (usually friend functions).
The second parameter is whatever new type it is being overloaded to print:
friend ostream& operator << (ostream& s, Fraction f);
This declaration has all of the usual parts for defining a
function. The name is operator<< (the keyword
operator and the operator symbol). The return type is
ostream&. The parameters are (ostream& s, Fraction
f). When defining overloads of << and >> , always pass the
stream parameters by reference. A better way to write this operator is:
friend ostream& operator << (ostream& s, const Fraction& f);
Notice that the first one passes the Fraction by value (and makes a
copy). The second passes by reference (avoiding the overhead of a
copy). It is declared as a const because the Fraction does not need
to change if we are just doing output.
Here is the corresponding prototype for extraction >>
friend istream& operator >> (istream& s, Fraction& f);
Notice that the Fraction parameter for >> is also a reference parameter.
This is because we are getting input into the object, so we need to work
on the original, not a copy.
Remember the Show() function of the Fraction class?
void Fraction::Show()
{
cout << numerator << '/' << denominator;
}
Here is how the << operator might be defined for Fraction.
Notice how similar it is to the Show() function.
ostream& operator << (ostream& s, const Fraction& f)
{
s << f.numerator << '/' << f.denominator;
return s;
}
Note the differences between this and the Show() function.
-
In this function, we must have a return statement, because we have a return
type (as opposed to "void" in the original Show function). We return
the ostream itself.
-
Note also that we must use "s" (not "cout") in the function body.
This is the formal parameter, and a nickname for whatever was passed in
(which could be cout, but also could be a different ostream).
-
Last, notice that this is not a member function of the Fraction class,
but rather, a friend function. So, we can access the private data,
but we must do it through the object:
Once this is defined, we can use a Fraction object in a cout statement:
Fraction f1;
So now, instead of:
cout << "Fraction f1 is ";
f1.Show();
cout << '\n';
We can write:
cout << "Fraction f1 is " << f1 << '\n';
Click here to see the Fraction class
with the << overload used instead of Show().
Now, what would the definition of the >> overload look like for Fraction?
Try it!
Another class example
class Complex
This example contains a Complex number class, for implementing complex
numbers, of the form a+bi, where a is the real part, b is the imaginary
part, and i is the square root of -1. The example illustrates the
following operators:
- Arithmetic operators ( + - * / )
- Insertion operator <<
- Extraction operator >>
- increment and decrement ( ++ -- )
The example also illustrates other concepts we've seen in classes:
- Default constructor
- Conversion constructor
- destructor
- accessor and mutator functions
There is also a sample starter test program. It does not yet test all
features of the class, but I will be adding to it soon.