Chapter 4 : Better Interfaces to Classes

Chapter 4 : Better Interfaces to Classes

Contents

4.1. Introduction

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.

4.2. Operator overloading

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.

The following fragment of code uses this method:

Compare this with the addition of two integers:

The integer addition is much more natural because:

  • i) it uses the standard + operator;
  • ii) it has a more natural sense of symmetry.

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

    A user program could now include the following code.

    The compiler would recognise that the + operator is being applied to Money objects and would automatically convert this statement to:

    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.

    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.

    4.3. An example - the Point Class

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

  • Constructor: construct from two floating point numbers.
  • Combination: binary addition and subtraction between Point objects; unary negation of Point objects; scaler multiplication and division of Point objects.
  • Tests: all comparisons (<, >, <=, >=, ==, !=); Test whether the value is (0,0).
  • Access: Return the x and y values; return the magnitude (distance from the origin); return the angle from the X axis; set to new values of x and y.
  • I/O: display and read in the standard form (X,Y); draw on the X/Y plane.

    Clearly, many of these methods are suitable candidates for operator overloading, resulting in the following class definition (file Point.h).

    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.

    Notice how we pass expressions to constructors as a convenient shorthand. For example, the implementation of operator+ includes :-

    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.

    Compiling this program requires the command: g++ route.C Point.o -lm (don't forget the maths library).

    4.4. Function overloading

    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,

    can be distinguished as separate functions. But,

    would both clash with:

    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:

    The following declarations would all be acceptable in a users program with the compiler matching arguments in order to determine which constructor to use.

    In fact, the user could also write:

    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:

    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.

    4.5. Overloading input and output operators

    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:

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

    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:

    And the compiler will map this into:

    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:

    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.

    The implementation of this operator is as follows.

    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.

    4.6. Friends

    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.

    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.

    Here is the implementation of the non-member function operator<<.

    Finally, here is the revised "route" program showing the use of the new operators.

    4.7. References

    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!