This chapter explores methods of making your own C++ classes easier and more intuitive for other programmers to use. By the end of the chapter you should be able to write classes which appear as if they were an integrated part of the C++ language.
Major topics include:
The chapter is based around the example of a Point class for representing and manipulating two dimensional coordinates.
Chapter one suggested that Abstract Data Types could support an extensible programming language where new general purpose data types could easily be introduced. Of course, the new ADTs should be as natural to use as the "inbuilt" data types (e.g. int and float in C++). However, this is not true of the ADTs we have seen so far. For example, the Money class of chapter two defines the method add to combine two money objects together.
Money add(Money m);
The following fragment of code uses this method:
Money a(1,30), b(2,40), c(0,0); c = a.add(b);
Compare this with the addition of two integers:
Int a = 0, b = 1, c; c = a + b;
The integer addition is much more natural because:
C++ overcomes this problem by allowing us to redefine standard operators to apply to our own classes. This is called operator overloading. The basic mechanism is quite simple. When defining the Money class, instead of calling our method add we could call it operator+.
Money operator+(Money m);
A user program could now include the following code.
Money a(1,30), b(2,40), c(0,0); c = a + b;
The compiler would recognise that the + operator is being applied to Money objects and would automatically convert this statement to:
c = a.operator+(b);
It is important to understand that this conversion actually takes place behind the scenes (see section 4.5 on overloading input/output operators).
Operator overloading is very flexible, and the operators that can be overloaded include the following.
+ - * / % < > <= >= == != && || !
We shall come across more later on in the course. The great danger with operator overloading is that implementors might overload operators to behave in unpredicatable counter-intuitive ways. Consequently, in order to avoid possible confusion, C++ places several constraints on the use of operator overloading.
First, the overloaded operator must retain the same "arity" as its standard definitions. For example, * must always be a binary operator and ! must always be unary. Of course, + and - can be both unary or binary. Thus, the following code fragment is illegal because it attempts to overload the + operator to have three arguments.
Money operator+(Money a, Money b); // THIS IS ILLEGAL!!!
Remember, there is always a first hidden argument which is the object on which the method is actually invoked!
Second, the priority of the operators will always remain the same. Thus, * always has higher priority than +, even when overloaded.
In general I would advise great caution when overloading. Only overload an operator when its effect clearly remains the same (e.g. * should always multiply things). Avoid making your classes harder to use by arbitrary overloading just for the sake of it.
As an additional comment, the argument types for overloaded operators can be anything you like provided the above rules are obeyed. Thus, I could define the following method in the Money class to add an integer number to a Money value.
Money operator+(int i);
This section illustrates operator overloading through a Point class representing two dimensional coordinates (e.g. (x,y) map references). This class might be very useful for solving a range of geometry problems or in graphics applications which have to deal with screen positions. You might also like to think about how it could be extended to three dimensional coordinates or even to a generalised Vector class.
First, we will very briefly skip through the design of the Point class (in reality, a long process). A Point consists of a pair of floating point numbers called X and Y. Considering the five categories of interface function, we arrive at the following:-
Clearly, many of these methods are suitable candidates for operator overloading, resulting in the following class definition (file Point.h).
// Point.h - definition of a Point class to represent coordinates // Steve Benford - November 1991 #ifndef POINT_H #define POINT_H class Point { private: float X,Y; // coordinates stored as floats public: // constructor Point(float x, float y); // build a Point with initial value // combination Point operator+(Point &p); // add two points together Point operator-(Point &p); // subtract two points Point operator*(float f); // scaler multiplication Point operator/(float f); // scalar division Point operator+(); // unary + Point operator-(); // unary - // tests int operator<(Point &p); // less than int operator>(Point &p); // greater than int operator<=(Point &p); // less than or equal to int operator>=(Point &p); // greater than or equal to int operator==(Point &p); // equal to int operator!=(Point &p); // not equal to int operator!(); // is zero // access float x(); // return x coordinate float y(); // return y coordinate float magnitude(); // return magnitude int angle(); // return angle from x axis // in the range -180 < angle <= 180 void set(float x, float y); // set to new value // I/O void print(); // print as (x,y) void draw(); // draw on axis void read(); // read from input as (x,y) }; #endif
Notice that all Point arguments to methods have been passed as reference arguments. This is for reasons of efficiency (see section 4.7).
The implementation of the Point class is given below.
// Point.C - implementation of the Point class // Steve Benford - November 1991 #include <stream.h> // use the maths library for trig functions #include <math.h> #include "Point.h" // construct a new point with initial values Point::Point(float x,float y) { X = x; Y = y; } // add this point to given point, p, and return result Point Point::operator+(Point &p) { // notice how we pass expressions to the constructor for result Point result(X + p.X, Y + p.Y); return result; } // subtract the given point, p, from this point and return the result Point Point::operator-(Point &p) { Point result(X - p.X, Y - p.Y); return result; } // scaler multiply for this point Point Point::operator*(float f) { Point result(X*f, Y*f); return result; } // scaler divide of this point Point Point::operator/(float f) { Point result(X/f, Y/f); return result; } // unary + (change sign to +ve) Point Point::operator+() { Point result(+X, +Y); return result; } // unary - (change sign to opposite) Point Point::operator-() { Point result(-X, -Y); return result; } // return the value of the x coordiante float Point::x() { return X; } // return the value of the y coordinate float Point::y() { return Y; } // return the magnitude of the point float Point::magnitude() { return sqrt(X*X + Y*Y); } // return the nearest whole number angle of the point with the X axis // return it in degrees in the range -180 < angle <= 180 int Point::angle() { float degrees; // atan2 returns radians so we must convert to degrees // by multiplying by 57/32 degrees = 57.32 * atan2(Y,X); return int(degrees); } // set new coordinates for this point void Point::set(float x, float y) { X = x; Y = y; } // test whether this point has greater magnitude than the given point p int Point::operator>(Point &p) { if(magnitude() > p.magnitude()) return 1; else return 0; } // test less than int Point::operator<(Point &p) { if(magnitude() < p.magnitude()) return 1; else return 0; } // greater equals int Point::operator>=(Point &p) { if(magnitude() >= p.magnitude()) return 1; else return 0; } // less equals int Point::operator<=(Point &p) { if( magnitude() <= p.magnitude()) return 1; else return 0; } // are two points equivalent int Point::operator==(Point &p) { if ((X == p.X) && (Y == p.Y)) return 1; else return 0; } // are two points not equivalent int Point::operator!=(Point &p) { if ((X != p.X) || (Y != p.Y)) return 1; else return 0; } // is this point equal to the origin (0,0) int Point::operator!() { if ((X == 0) && (Y == 0)) return 1; else return 0; } // print this point in the form "(x,y)" void Point::print() { cout << "(" << X << "," << Y << ")"; } // draw this point on the X,Y plane // not implemented yet!!! void Point::draw() { cout << "(" << X << "," << Y << ")"; } // read in this point in the form "(x,y)" void Point::read() { char l,c,r; // read in brackets, comma and values cin >> l >> X >> c >> Y >> r; // check for a format error if ( (l != '(') || (c != ',') || (r != ')') ) { // if so, give warning and set to (0,0) cerr << "Point::read input format error\\n"; X = 0; Y = 0; } }
Notice how we pass expressions to constructors as a convenient shorthand. For example, the implementation of operator+ includes :-
Point result(X + p.X, Y + p.Y);
Observe that the comparison operators are easily built on top of the magnitude function.
The standard mathematics function atan2 is used to find the angle (in radians) of the point from the X-axis given its tangent. This is then converted to degrees. In order to use this function, the header file <math.h> is included. In addition the -lm flag must be passed to the compiler in order to include the binary for this function when a user program is compiled.
The read() function carefully checks the input format of a Point. However, draw() has not been implemented properly (I have just copied print()). You might like to think about how to do this yourselves.
The following program tests the Point class. It reads in a series of Points representing a route. The +, - and magnitude methods are then used to calculate the total distance travelled and also the distance from start to finish. Notice how easy it is to use the Point class.
// File route.C // Read in a starting point and a series of coordinates. // Calculate the total distance travelled as well as the distance // between start and finish points #include <stream.h> #include "Point.h" #include <math.h> main() { // initial, previous and next points on the journey Point start(0,0), prev(0,0), next(0,0); char c; float distance = 0; // record total distance travelled cout << "Please enter your initial point\\n"; start.read(); prev = start; do { cout << "enter the next point on your journey\\n"; next.read(); distance = distance + (next - prev).magnitude(); prev = next; cout << "do you wish to enter another point? (y/n)\\n"; cin >> c; } while( c == 'y'); cout << "Total distance travelled " << distance << "\\n"; cout << "Distance between start and finish " << (next - start).magnitude() << "\\n"; }
Compiling this program requires the command: g++ route.C Point.o -lm (don't forget the maths library).
The general term "overloading" means using the same name to mean different things in different contexts. To recap, the same name can be used for different functions provided that the compiler can unambiguously match each function call from the number and types of its arguments. Remember, the type of the result and the names of the arguments are not taken into account. Thus,
int max(int a, int b); int max(int a, int b, int c); float max(float a, float b);
can be distinguished as separate functions. But,
float max(int a, int b); int max(int x, int y);
would both clash with:
int max(int a, int b);
An important use of function overloading is in defining multiple constructors for a class. We often want a choice of how to initialise objects. For example, a Point could be initialised to the value (0,0), or to two specific values (x,y) or to the value of another Point. We could offer the user a choice by implementing the following three constructors:
Point(float x, float y); // initialise to given values Point(); // initialise to 0 Point(Point &p); // initialise to another Point
The following declarations would all be acceptable in a users program with the compiler matching arguments in order to determine which constructor to use.
Point a; Point b(2.1, -3); Point c(a);
In fact, the user could also write:
Point d = a;
and the compiler would recognise a call to Point(Point &p);. We call this last constructor the copy constructor because it copies one object to another of the same type.
We could also get the effect of the two constructors Point() and Point(float x, float y) by using default parameters. In this case we would define the constructor:
Point(float x = 0, float y = 0);
It is a matter of personal taste which approach you chose. It is left as an exercise for the student to look back over previous classes and to define additional constructors where appropriate.
The test program for the Point class demonstrates that operator overloading makes classes much easier to use. However, input and output still require specialised methods such as print() and read(). Ideally, we would like to use the insertion (<<) and extraction (>>) operators on stream objects (cin, cout, cerr) just as we do with inbuilt types. This can be achieved by overloading >> and <<. However, it is rather more complex than for other operators.
A user of the Point class wishes to write code such as:
Point a, b, c; cin >> a; cin >> b; cout << a + b;
Consider the statement cin >> a in more detail. This would use the overloaded operator >> to read in a Point object. Section 4.2 told us that the compiler would map this onto the statement cin.operator>>(a). This implies that we have to implement a new method operator>> which can be invoked on the object cin. Now cin is an object of class iostream (stands for "input stream") which is defined in the file <stream.h>. Unfortunately, this class definition is not ours to modify so we cannot write the overloaded member function!
One approach to this problem might be to define operator>> on the Point object. However, the user would have to write a >> cin. This is completely counter-intuitive and therefore a bad idea.
The solution to our problem lies in a new C++ technique called friends. We can define the following non-member function called operator>>.
iostream& operator>>(iostream& is, Point p);
We call this a non-member function because it is not defined inside a specific class (in contrast to member-functions). The functions you saw in PR1 were all non-member functions. The first argument of the function is an object of class iostream (e.g. cin) the second is an object of class Point. The user can now write:
Point a; cin >> a;
And the compiler will map this into:
operator>>(cin, a);
In the general case we can apply operator overloading to both member and non-member functions. It is also true that any overloaded operator can be defined as either a member or non-member function. This choice is largely a matter of personal preference (the course examples use member functions where possible).
Notice that our overloaded operator returns a result of class iostream. This makes it possible for the user to write statements such as:
Point a, b; int i, j; cin >> a >> i >> b >> j;
Effectively these will be executed as a series of nested statements, with the result of each (cin) being used as the left side argument of the next.
(((cin >> a) >> i) >> b) >> j;
The implementation of this operator is as follows.
// overloaded input operator (friend function) // "used to be read" istream& operator>>(istream& is, Point& p) { char l,c,r; // read from input stream is >> l >> p.X >> c >> p.Y >> r; // check format if ( (l != '(') || (c != ',') || (r != ')') ) { cerr << "Point::read input format error\\n"; p.X = 0; p.Y = 0; } // NOTE: we need to return the input stream return is; }
Notice how it resembles the read() function defined previously. Also notice how the input stream given as its argument is returned as its result. The prefix Point::does not appears before the function name because this is not a method of class Point.
There is still a problem with our overloaded input operator. Chapter 2 stated that a major feature of classes is that they don't allow external functions to access the private data inside the class. This means that our non-member function operator>> will not actually be allowed to alter that values p.X and p.Y.
This problem can be solved by using a technique called friends. In the class definition for Point we can specify that our new function is a "friend" of the class through the following statement.
friend istream &operator>>(istream& is, Point& p);
This gives the function permission to access private data members and functions of the class. In other words, being a friend to a class over-rides the protection mechanism.
In general, any non-member function can be declared as a friend. Futhermore, whole classes can be declared as friends meaning that any of their methods can access private members (more about this in chapter 7).
In general, use of the friends technique is bad form. Over-riding the protection mechanism provided by classes increases the chances of bugs and results in badly structured programs. I would discourage it except for exceptional circumstances such as overloading input and output operators. Treat friends as you would gotos!
The following is a revised class definition for the Point class which defines overloaded input and output operators to replace the member functions print() and read(). Note the use of the friends mechanism.
// File PointIO.h // REVISED Point.h - definition of a class to represent coordinates // revision replaces the member functions "print" and "read" with // overloaded ">>" and "<<" operators. // Steve Benford - November 1991 #ifndef POINT_H #define POINT_H class Point { private: float X,Y; // coordinates stored as floats public: // constructor Point(float x, float y); // build a Point with initial value // combination Point operator+(Point &p); // add two points together Point operator-(Point &p); // subtract two points Point operator*(float f); // scaler multiplication Point operator/(float f); // scalar division Point operator+(); // unary + Point operator-(); // unary - // tests int operator<(Point &p); // less than int operator>(Point &p); // greater than int operator<=(Point &p); // less than or equal to int operator>=(Point &p); // greater than or equal to int operator==(Point &p); // equal to int operator!=(Point &p); // not equal to int operator!(); // is zero // access float x(); // return x coordinate float y(); // return y coordinate float magnitude(); // return magnitude int angle(); // return angle from x axis // in the range -180 < angle <= 180 void set(float x, float y); // set to new value // I/O // overloaded output operator friend ostream &operator<<(ostream& os, Point p); // overloaded input operator friend istream &operator>>(istream& is, Point& p); void read(); // read from input as (x,y) void draw(); // draw on the X/Y plane }; #endif
Here is the implementation of the non-member function operator<<.
// overloaded output operator (friend function) // used to be "print" ostream& operator<<(ostream& os, Point p) { // print on output stream os << "(" << p.X << "," << p.Y << ")"; // return output stream return os; }
Finally, here is the revised "route" program showing the use of the new operators.
// Read in a starting point and a series of coordinates. // Calculate the total distance travelled as well as the distance // between start and finish points #include <stream.h> #include "PointIO.h" #include <math.h> main() { // initial, previous and next points on the journey Point start(0,0), prev(0,0), next(0,0); char c; float distance = 0; // record total distance travelled cout << "Please enter your initial point\\n"; cin >> start; prev = start; do { cout << "enter the next point on your journey\\n"; cin >> next; distance = distance + (next - prev).magnitude(); prev = next; cout << "do you wish to enter another point? (y/n)\\n"; cin >> c; } while( c == 'y'); cout << "Total distance travelled " << distance << "\\n"; cout << "Distance between start and finish " << (next - start).magnitude() << "\\n"; }
The examples in the chapter have been using the reference symbol (&) to pass function arguments and results. References were introduced in PR1 as a means to alter the argument passing mechanism and to enable "side-effects" where a function can alter values in the outside program. The swap function was given as a classic example of this use of references.
Copying objects during the default "pass-by-value" mechanism can be very time consuming for complex classes. Using references avoids this copying and so also increase the speed of function calls. Thus, a second use of references is to increase the execution speed of programs that pass classes as function arguments. This is the use we are seeing in the Point class.
This use of references is generally a good idea, provided that the user and implementor are aware of possible side-effects that may take place.
Notes converted from troff to HTML by an Eric Foxley shell script, email errors to me!