CONTENTS

Chapter 11. WORKING WITH CLASSES

You will learn about the following in this chapter:

C++ classes are feature-rich, complex, and powerful. In Chapter 9, "Memory Models and Namespaces," you began a journey toward object-oriented programming by learning to define and use a simple class. You saw how a class defines a data type by defining the type of data to be used to represent an object and by also defining, through member functions, the operations that can be performed with that data. And you learned about two special member functions, the constructor and the destructor, that manage creating and discarding objects made to a class specification. This chapter will take you a few steps further in the exploration of class properties, concentrating on class design techniques rather than on general principles. You'll probably find some of the features covered here straightforward, some a bit more subtle. To best understand these new features, you should try the examples and experiment with them. What happens if I use a regular argument instead of a reference argument for this function? What happens if I leave something out of a destructor? Don't be afraid to make mistakes; usually you can learn more from unraveling an error than by doing something correctly, but by rote. (However, don't assume that a maelstrom of mistakes inevitably leads to incredible insight.) In the end, you'll be rewarded with a fuller understanding of how C++ works and of what C++ can do for you.

This chapter starts with operator overloading, which lets you use standard C++ operators such as = and + with class objects. Then it examines friends, the C++ mechanism for letting nonmember functions access private data. Finally, it looks at how you can instruct C++ to perform automatic type conversions with classes. As you go through this and the next chapter, you'll gain a greater appreciation of the roles class constructors and class destructors play. Also, you'll see some of the stages you may go through as you develop and improve a class design.

One difficulty with learning C++, at least by the time you've gotten this far into the subject, is that there is an awful lot to remember. And it's unreasonable to expect to remember it all until you've logged enough experience on which to hang your memories. Learning C++, in this respect, is like learning a feature-laden word processor or spreadsheet program. No one feature is that daunting, but, in practice, most people really know well only those features they use regularly, such as searching for text or italicizing. You may recall having read somewhere how to generate alternative characters or create a table of contents, but those skills probably won't be part of your daily repertoire until you find yourself in a situation in which you need them frequently. Probably the best approach to absorbing the wealth of material in this chapter is to begin incorporating just some of these new features into your own C++ programming. As your experiences enhance your understanding and appreciation of these features, begin adding other C++ features. As Bjarne Stroustrup, the creator of C++, suggested at a C++ conference for professional programmers: "Ease yourself into the language. Don't feel you have to use all of the features, and don't try to use them all on the first day."

Operator Overloading

Let's look at a technique for giving object operations a prettier look. Operator overloading is another example of C++ polymorphism. In Chapter 8, "Adventures in Functions," you saw how C++ enables you to define several functions having the same name as long as they have different signatures (argument lists). That was function overloading, or functional polymorphism. Its purpose is to let you use the same function name for the same basic operation even though you apply the operation to different data types. (Imagine how awkward English would be if you had to use a different verb form for each different type of object條ift_lft your left foot, but lift_sp your spoon.) Operator overloading extends the overloading concept to operators, letting you assign multiple meanings to C++ operators. Actually, many C++ (and C) operators already are overloaded. For example, the * operator, when applied to an address, yields the value stored at that address. But applying * to two numbers yields the product of the values. C++ uses the number and type of operands to decide which action to take.

C++ lets you extend operator overloading to user-defined types, permitting you, say, to use the + symbol to add two objects. Again, the compiler will use the number and type of operands to determine which definition of addition to use. Overloaded operators often can make code look more natural. For example, a common computing task is adding two arrays. Usually, this winds up looking like the following for loop:

for (int i = 0; i < 20; i++)
       evening[i] = sam[i] + janet[i];  // add element by element

But in C++, you can define a class that represents arrays and that overloads the + operator so that you can do this:

evening = sam + janet;                 // add two array objects

This simple addition notation conceals the mechanics and emphasizes what is essential, and that is another OOP goal.

To overload an operator, you use a special function form called an operator function. An operator function has the form:

operatorop(argument-list)

where op is the symbol for the operator being overloaded. That is, operator+() overloads the + operator (op is +) and operator*() overloads the * operator (op is *). The op has to be a valid C++ operator; you can't just make up a new symbol. For example, you can't have an operator@() function because C++ has no @ operator. But the operator[]() function would overload the [] operator because [] is the array-indexing operator. Suppose, for example, that you have a Salesperson class for which you define an operator+() member function to overload the + operator so that it adds sales figures of one salesperson object to another. Then, if district2, sid, and sara all are objects of the Salesperson class, you can write this equation:

district2 = sid + sara;

The compiler, recognizing the operands as belonging to the Salesperson class, will replace the operator with the corresponding operator function:

district2 = sid.operator+(sara);

The function will then use the sid object implicitly (because it invoked the method) and the sara object explicitly (because it's passed as an argument) to calculate the sum, which it then returns. Of course, the nice part is that you can use the nifty + operator notation instead of the clunky function notation.

C++ imposes some restrictions on operator overloading, but they're easier to understand after you've seen how overloading works. So let's develop a few examples first to clarify the process and then discuss the limitations.

Time on Our Hands

If you worked on the Priggs account 2 hours 35 minutes in the morning and 2 hours 40 minutes in the afternoon, how long did you work altogether on the account? Here's an example where the concept of addition makes sense, but the units that you are adding (a mixture of hours and minutes) doesn't match a built-in type. Chapter 7, "Functions桟++'s Programming Modules," handled a similar case by defining a travel_time structure and a sum() function for adding such structures. Now we can generalize that to a Time class using a method to handle addition. Let's begin with an ordinary method, called Sum(), then see how to convert it to an overloaded operator. Listing 11.1 shows the class declaration.

Listing 11.1 mytime0.h
// mytime0.h -- Time class before operator overloading
#ifndef MYTIME0_H_
#define MYTIME0_H_
#include <iostream>
using namespace std;
class Time
{
private:
    int hours;
    int minutes;
public:
    Time();
    Time(int h, int m = 0);
    void AddMin(int m);
    void AddHr(int h);
    void Reset(int h = 0, int m = 0);
    Time Sum(const Time & t) const;
    void Show() const;
};
#endif

The class provides methods for adjusting and resetting times, for displaying time values, and for adding two times. Listing 11.2 shows the methods definitions; note how the AddMin() and Sum() methods use integer division and the modulus operator to adjust the minutes and hours values when the total number of minutes exceeds 59.

Listing 11.2 mytime0.cpp
// mytime0.cpp  -- implement Time methods
#include "mytime0.h"

Time::Time()
{
    hours = minutes = 0;
}

Time::Time(int h, int m )
{
    hours = h;
    minutes = m;
}

void Time::AddMin(int m)
{
    minutes += m;
    hours += minutes / 60;
    minutes %= 60;
}
void Time::AddHr(int h)
{
    hours += h;
}

void Time::Reset(int h, int m)
{
    hours = h;
    minutes = m;
}

Time Time::Sum(const Time & t) const
{
    Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}

void Time::Show() const
{
    cout << hours << " hours, " << minutes << " minutes";
    cout << '\n';
}

Consider the code for the Sum() function. Note that the argument is a reference but that the return type is not a reference. The reason for making the argument a reference is efficiency. The code would produce the same results if the Time object were passed by value, but it's usually faster and more memory-efficient to just pass a reference.

The return value, however, cannot be a reference. The reason is that the function creates a new Time object (sum) representing the sum of the other two Time objects. Returning the object, as this code does, creates a copy of the object that the calling function can use. If the return type were Time &, however, the reference would be to the sum object. But the sum object is a local variable and is destroyed when the function terminates, so the reference would be a reference to a nonexistent object. Using a Time return type, however, means the program constructs a copy of sum before destroying it, and the calling function gets the copy.

Caution

graphics/tnt.gif

Don't return a reference to a local variable or other temporary object.

Finally, Listing 11.3 tests the time summation part of the class.

Listing 11.3 usetime0.cpp
// usetime0.cpp -- use first draft of Time class
// compile usetime0.cpp and mytime0.cpp together
#include <iostream>
#include "mytime0.h"
using namespace std;

int main()
{
    Time A;
    Time B(5, 40);
    Time C(2, 55);

    cout << "A = ";
    A.Show();
    cout << "B = ";
    B.Show();
    cout << "C = ";
    C.Show();

    A = B.Sum;
    cout << "B.Sum = ";
    A.Show();
    return 0;
}

Here is the output:

A = 0 hours, 0 minutes
B = 5 hours, 40 minutes
C = 2 hours, 55 minutes
B.Sum = 8 hours, 35 minutes

Adding an Addition Operator

It's a simple matter to convert the Time class to using an overloaded addition operator. Just change the name of Sum() to the odder-looking name operator+(). That's right; just append the operator symbol (+, in this case) to end of operator and use the result as a method name. This is one place where you can use a character other than a letter, digit, or underscore in an identifier name. Listings 11.4 and 11.5 reflect this small change.

Listing 11.4 mytime1.h
// mytime1.h -- Time class after operator overloading
#ifndef MYTIME1_H_
#define MYTIME1_H_
#include <iostream>
using namespace std;

class Time
{
private:
    int hours;
    int minutes;
public:
    Time();
    Time(int h, int m = 0);
    void AddMin(int m);
    void AddHr(int h);
    void Reset(int h = 0, int m = 0);
    Time operator+(const Time & t) const;
    void Show() const;
};
#endif
Listing 11.5 mytime1.cpp
// mytime1.cpp  -- implement Time methods
#include "mytime1.h"

Time::Time()
{
    hours = minutes = 0;
}

Time::Time(int h, int m )
{
    hours = h;
    minutes = m;
}

void Time::AddMin(int m)
{
    minutes += m;
    hours += minutes / 60;
    minutes %= 60;
}
void Time::AddHr(int h)
{
    hours += h;
}

void Time::Reset(int h, int m)
{
    hours = h;
    minutes = m;
}

Time Time::operator+(const Time & t) const
{
    Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}

void Time::Show() const
{
    cout << hours << " hours, " << minutes << " minutes";
    cout << '\n';
}

Like Sum(), operator+() is invoked by a Time object, takes a second Time object as an argument, and returns a Time object. Thus, you can invoke the operator+() method using the same syntax that Sum() used:

A = B.operator+;    // function notation

But naming the method operator+() also lets you use operator notation:

A = B + C;              // operator notation

Either notation invokes the operator+() method. Note that with the operator notation, the object to the left of the operator (B, in this case) is the invoking object, and the object to the right (C, in this case) is the one passed as an argument. Listing 11.6 illustrates this point.

Listing 11.6 usetime1.cpp
// usetime1.cpp -- use second draft of Time class
// compile usetime1.cpp and mytime1.cpp together
#include <iostream>
#include "mytime1.h"
using namespace std;

int main()
{
    Time A;
    Time B(5, 40);
    Time C(2, 55);

    cout << "A = ";
    A.Show();
    cout << "B = ";
    B.Show();
    cout << "C = ";
    C.Show();

    A = B.operator+;  // function notation
    cout << "A = B.operator+ = ";
    A.Show();
    B = A + C;           // operator notation
    cout << "A + C = ";
    B.Show();    return 0;
}

Here is the output:

A = 0 hours, 0 minutes
B = 5 hours, 40 minutes
C = 2 hours, 55 minutes
A = B.operator+ = 8 hours, 35 minutes
A + C = 11 hours, 30 minutes

In short, the name of the operator+() function allows it to be invoked using either function notation or operator notation. The compiler uses the operand types to figure out what to do:

int a, b, c;
Time A, B, C;
c = a + b;      // use int addition
C = A + B;      // use addition as defined for Time objects

Overloading Restrictions

Most C++ operators (see Table 11.1) can be overloaded in the same manner. Overloaded operators (with some exceptions) don't necessarily have to be member functions. However, at least one of the operands has to be a user-defined type. Let's take a closer look at the limits C++ imposes on user-defined operator overloading:

  1. The overloaded operator must have at least one operand that is a user-defined type. This prevents you from overloading operators for the standard types. Thus, you can't redefine the minus operator (-) so that it yields the sum of two double values instead of their difference. This restriction preserves program sanity, although it may hinder creative accounting.

  2. You can't use an operator in a manner that violates the syntax rules for the original operator. For example, you can't overload the modulus operator (%) so that it can be used with a single operand:

    int x;
    Time shiva;
    % x;       // invalid for modulus operator
    % shiva;   // invalid for overloaded operator
    

    Similarly, you can't alter operator precedence. So if you overload the addition operator to let you add two classes, the new operator has the same precedence as ordinary addition.

  3. You can't create new operator symbols. For example, you can't define an operator**() function to denote exponentiation.

  4. You cannot overload the following operators:

    sizeof The sizeof operator
    . Membership operator
    .* Pointer-to-member operator
    :: Scope resolution operator
    ?: Conditional operator
    typeid An RTTI operator
    const_cast A type cast operator
    dynamic_cast A type cast operator
    reinterpret_cast A type cast operator
    static_cast A type cast operator

    This still leaves all the operators in Table 11.1 available for overloading.

  5. Most of the operators in Table 11.1 can be overloaded by using either member or nonmember functions. However, you can use only member functions to overload the following operators:

    = Assignment operator
    () Function call operator
    [] Subscripting operator
    -> Class member access by pointer operator

Note

graphics/common.gif

We have not covered, nor will we cover, every operator mentioned in the list of restrictions or in Table 11.1. However, Appendix E, "Other Operators," does summarize those operators not covered in the main body of this text.

Table 11.1. Operators That Can Be Overloaded
+ - * / % ^& |

~=

*=

! = < > += -=

/=

>>=

%= ^= &= |= << >>

<<=

++

== != <= >= && ||

?/tt>

delete

, ->* -> () [] new
new [] delete []          

In addition to these formal restrictions, you should use sensible restraint in overloading operators. For example, don't overload the * operator so that it swaps the data members of two Time objects. Nothing in the notation would suggest what the operator did, so it would be better to define a class method with an explanatory name like Swap().

More Overloaded Operators

Some other operations make sense for the Time class. For example, you might want to subtract one time from another or multiply a time by a factor. This suggests overloading the subtraction and multiplication operators. The technique is the same as for the addition operator梒reate operator-() and operator*() methods. That is, add the following prototypes to the class declaration:

Time operator-(const Time & t) const;
Time operator*(double n) const;

Listing 11.7 shows the new header file.

Listing 11.7 mytime2.h
// mytime2.h -- Time class after operator overloading
#ifndef MYTIME2_H_
#define MYTIME2_H_
#include <iostream>
using namespace std;

class Time
{
private:
    int hours;
    int minutes;
public:
    Time();
    Time(int h, int m = 0);
    void AddMin(int m);
    void AddHr(int h);
    void Reset(int h = 0, int m = 0);
    Time operator+(const Time & t) const;
    Time operator-(const Time & t) const;
    Time operator*(double n) const;
    void Show() const;
};
#endif.

Then add definitions for the new methods to the implementation file, as shown in Listing 11.8.

Listing 11.8 mytime2.cpp
// mytime2.cpp  -- implement Time methods
#include "mytime2.h"

Time::Time()
{
    hours = minutes = 0;
}

Time::Time(int h, int m )
{
    hours = h;
    minutes = m;
}

void Time::AddMin(int m)
{
    minutes += m;
    hours += minutes / 60;
    minutes %= 60;
}
void Time::AddHr(int h)
{
    hours += h;
}

void Time::Reset(int h, int m)
{
    hours = h;
    minutes = m;
}

Time Time::operator+(const Time & t) const
{
    Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}

Time Time::operator-(const Time & t) const
{
    Time diff;
    int tot1, tot2;
    tot1 = t.minutes + 60 * t.hours;
    tot2 = minutes + 60 * hours;
    diff.minutes = (tot2 - tot1) % 60;
    diff.hours = (tot2 - tot1) / 60;
    return diff;
}
Time Time::operator*(double mult) const
{
    Time result;
    long totalminutes = hours * mult * 60 + minutes * mult;
    result.hours = totalminutes / 60;
    result.minutes = totalminutes % 60;
    return result;
}

void Time::Show() const
{
    cout << hours << " hours, " << minutes << " minutes";
    cout << '\n';
}

With these changes made, you can test the new definitions with the code shown in Listing 11.9.

Listing 11.9 usetime2.cpp
// usetime2.cpp -- use third draft of Time class
// compile usetime2.cpp and mytime2.cpp together
#include <iostream>
#include "mytime2.h"
using namespace std;

int main()
{
    Time A;
    Time B(5, 40);
    Time C(2, 55);

    cout << "A = ";
    A.Show();
    cout << "B = ";
    B.Show();
    cout << "C = ";
    C.Show();

    A = B + C;     // operator+()
    cout << "B + C = ";
    A.Show();
    A = B - C;     // operator-()
    cout << "B - C = ";
    A.Show();
    A = B * 2.75;  // operator*()
    cout << "B * 2.75 = ";
    A.Show();
    return 0;
}

Here is the output:

A = 0 hours, 0 minutes
B = 5 hours, 40 minutes
C = 2 hours, 55 minutes
B + C = 8 hours, 35 minutes
B - C = 2 hours, 45 minutes
B * 2.75 = 15 hours, 35 minutes

Introducing Friends

As you've seen, C++ controls access to the private portions of a class object. Usually public class methods serve as the only access, but sometimes this restriction is too rigid to fit particular programming problems. In such cases, C++ provides another form of access, the friend. Friends come in three varieties:

By making a function a friend to a class, you allow the function the same access privileges that a member function of the class has. We'll look into friend functions now, leaving the other two varieties to Chapter 15, "Friends, Exceptions, and More."

Before seeing how to make friends, let's look into why they might be needed. Often, overloading a binary operator (one with two arguments) for a class generates a need for friends. Multiplying a Time object by a real number provides just such a situation, so let's study that case.

In the Time class example, the overloaded multiplication operator is different from the other two overloaded operators in that it combines two different types. That is, the addition and subtraction operators each combine two Time values, but the multiplication operator combines a Time value with a double value. This restricts how the operator can be used. Remember, the left operand is the invoking object. That is,

A = B * 2.75;

translates to the following member function call:

A = B.operator*(2.75);

But what about the following statement?

A = 2.75 * B;     // cannot correspond to a member function

Conceptually, 2.75 * B should be the same as B * 2.75, but the first expression cannot correspond to a member function because 2.75 is not a type Time object. Remember, the left oper and is the invoking object, but 2.75 is not an object. So the compiler cannot replace the expression with a member function call.

One way around this difficulty is to tell everyone (and to remember yourself) that you can only write B * 2.75 but never write 2.75 * B. This is a programmer-friendly, user-beware solution, and that's not what OOP is about.

However, there is another possibility梐 nonmember function. (Remember, most operators can be overloaded using either member or nonmember functions.) A nonmember function is not invoked by an object; instead, any values it uses, including objects, are explicit arguments. Thus, the compiler could match the expression

A = 2.75 * B;     // cannot correspond to a member function

to the following nonmember function call:

A = operator*(2.75, B);

The function would have this prototype:

Time operator*(double m, const Time & t);

With the nonmember overloaded operator function, the left operand of an operator expression corresponds to the first argument of the operator function, and the right operand corresponds to the second argument. Meanwhile, the original member function will handle operands in the opposite order, that is, a Time value times a double value.

Using a nonmember function solves the problem of getting the operands in the desired order (first double, then Time), but it raises a new problem: Nonmember functions can't directly access private data in a class. Well, at least ordinary nonmember functions lack access. But there is a special category of nonmember function called a friend that can access private members of a class.

Creating Friends

The first step toward creating a friend function is to place a prototype in the class declaration, prefixing the declaration with the keyword friend:

friend Time operator*(double m, const Time & t);  // goes in class declaration

This prototype has two implications:

The second step is to write the function definition. Since it is not a member function, don't use the Time:: qualifier. Also, don't use the friend keyword in the definition:

Time operator*(double m, const Time & t)  // friend not used in definition
{
    Time result;
    long totalminutes = t.hours * mult * 60 +t. minutes * mult;
    result.hours = totalminutes / 60;
    result.minutes = totalminutes % 60;
    return result;
}

With this declaration and definition, the statement

A = 2.75 * B;

translates to

A = operator*(2.75, B);

and invokes the nonmember friend function we just defined.

In short, a friend function to a class is a nonmember function that has the same access rights as a member function.

Are Friends Unfaithful to OOP?

graphics/common.gif

At first glance, it might seem that friends violate the OOP principle of data hiding, for the friend mechanism allows nonmember functions to access private data. However, that's an overly narrow view. Instead, think of friend functions as part of an extended interface for a class. For example, from a conceptual point of view, multiplying a double times a Time value is pretty much the same as multiplying a Time value times a double. That the first requires a friend function whereas the second can be done with a member function is the result of C++ syntax, not of a deep conceptual difference. By using both a friend function and a class method, you can express either operation with the same user interface. Also, keep in mind that only a class declaration can decide which functions are friends, so the class declaration still controls which functions access private data. In short, class methods and friends are just two different mechanisms for expressing a class interface.

Actually, this particular friend function can be written as non-friend by altering the definition as follows:

Time operator*(double m, const Time & t)
{
    return t * m;     // use a member function
}

The original version accessed t.minutes and t.hours explicitly, so it had to be a friend. This version only uses the Time object t as a whole, letting a member function handle the private values, so this version doesn't have to be a friend. Nonetheless, it's still a good idea to make this version a friend, too. Most importantly, it ties the function in as part of the official class interface. Second, if you later find a need for the function to access private data directly, you only have to change the function definition and not the class prototype.

Tip

graphics/bulb.gif

If you want to overload an operator for a class and you want to use the operator with a nonclass term as the first operand, you can use a friend function to reverse the operand order.

A Common Kind of Friend: Overloading the << Operator

One of the more useful features of classes is that you can overload the << operator so that you can use it with cout to display an object's contents. In some ways, this overloading is a bit trickier than the earlier examples, so we'll develop it in two steps instead of in one.

Suppose trip is a Time object. To display Time values, we've been using Show (). Wouldn't it be nice, however, if you could do the following?

cout << trip;  // make cout recognize Time class?

You can, because << is one of the C++ operators that can be overloaded. In fact, it already is heavily overloaded. In its most basic incarnation, the << operator is one of C and C++'s bit manipulation operators; it shifts bits left in a value (see Appendix E). But the ostream class overloads the operator, converting it into an output tool. Recall that cout is an ostream object and that it is smart enough to recognize all the basic C++ types. That's because the ostream class declaration includes an overloaded operator<<() definition for each of the basic types. That is, one definition uses an int argument, one uses a double argument, and so on. So, one way to teach cout to recognize a Time object is to add a new function operator definition to the ostream class declaration. But it's not a good idea to alter the iostream file and mess around with a standard interface. It's better to use the Time class declaration to teach the Time class how to use cout.

First Version of Overloading <<

To teach the Time class to use cout, you'll have to use a friend function. Why? Because a statement like

cout << trip;

uses two objects, with the ostream class object (cout) first. If you use a Time member function to overload <<, the Time object would come first, as it did when we overloaded the * operator with a member function. That means you would have to use the << operator this way:

trip << cout;   // if operator<<() were a Time member function

That would be confusing. But by using a friend function, you can overload the operator this way:

void operator<<(ostream & os, const Time & t)
{
    os << t.hours << " hours, " << t.minutes << " minutes";
}

This lets you use

cout << trip;

to print data in the following format:

4 hours, 23 minutes

Friend or No Friend?

graphics/common.gif

The new Time class declaration makes the operator<<() function a friend function to the Time class. But this function, although not inimical to the ostream class, is not a friend to it. The operator<<() function takes an ostream argument and a Time argument, so it might seem this function has to be friends to both classes. If you look at the code for the function, however, you'll notice that the function accesses individual members of the Time object but only uses the ostream object as a whole. Because operator<<() accesses private Time object members directly, it has to be a friend to the Time class. But because it does not directly access private ostream object members, the function does not have to be a friend to the ostream class. That's nice, for it means you don't have to tinker with the ostream definition.

Note that the new operator<<() definition uses an ostream reference os as its first argument. Normally, os will refer to the cout object, as it does in the expression cout << trip. But you could use the operator with other ostream objects, in which case os would refer to those objects. (What? You don't know of any other ostream objects? Don't forget cerr, introduced in Chapter 10, "Objects and Classes." Also, in Chapter 17, "Input, Output, and Files," you'll learn how to create new objects to manage output to files, and these objects can use ostream methods. You then can use the operator<<() definition to write Time data to files as well as to the screen.) Furthermore, the call cout << trip should use the cout object itself, not a copy, so the function passes the object as a reference instead of by value. Thus, the expression cout << trip causes os to be an alias for cout, and the expression cerr << trip causes os to be an alias for cerr. The Time object can be passed by value or by reference, because either form makes the object values available to the function. Again, passing by reference uses less memory and time than passing by value.

Second Version of Overloading <<

The implementation we just presented has a problem. Statements such as

cout << trip;

work fine, but the implementation doesn't allow you to combine the redefined << operator with the ones cout normally uses:

cout << "Trip time: " << trip << " (Tuesday)\n"; // can't do

To understand why this doesn't work and what must be done to make it work, you first need to know a bit more about how cout operates. Consider the following statements:

int x = 5;
int y = 8;
cout << x << y;

C++ reads the output statement from left to right, meaning it is equivalent to the following:

(cout << x) << y;

The << operator, as defined in iostream, takes an ostream object to its left. Clearly, the expression cout << x satisfies that requirement because cout is an ostream object. But the output statement also requires that the whole expression (cout << x) be a type ostream object, because that expression is to the left of << y. Therefore, the ostream class implements the operator<<() function so that it returns an ostream object. In particular, it returns the invoking object, cout, in this case. Thus, the expression (cout << x) is itself an ostream object, and it can be used to the left of the << operator.

You can take the same approach with the friend function. Just revise the operator<<() function so that it returns a reference to an ostream object:

ostream & operator<<(ostream & os, const Time & t)
{
    os << t.hours << " hours, " << t.minutes << " minutes";
    return os;
}

Note that the return type is ostream &. That means, recall, that the function returns a reference to an ostream object. Because a program passes an object reference to the function to begin with, the net effect is that the function's return value is just the object passed to it. That is, the statement

cout << trip;

becomes the following function call:

operator<<(cout, trip);

And that call returns the cout object. So now the following statement does work:

cout << "Trip time: " << trip << " (Tuesday)\n"; // can do

Let's break this into separate steps to see how it works. First,

cout << "Trip time: "

invokes the particular ostream definition of << that displays a string and returns the cout object, so the expression cout << "Trip time: " displays the string and then is replaced by its return value, cout. This reduces the original statement to the following one:

cout << trip << " (Tuesday)\n";

Next, the program uses the Time declaration of << to display the trip values and to return the cout object again. This reduces the statement to the following:

cout << " (Tuesday)\n";

The program now finishes up by using the ostream definition of << for strings to display the final string.

Tip

graphics/bulb.gif

In general, to overload the << operator to display an object of class c_name, use a friend function with a definition of this form:

ostream & operator<<(ostream & os, const c_name & obj)
{
    os << ... ;  // display object contents
    return os;
}

Listing 11.10 shows the class definition as modified to include the two friend functions operator*() and operator<<(). It implements the first as an inline function because the code is so short. (When the definition also is the prototype, as in this case, you do use the friend prefix.)

Remember

graphics/arrow.gif

You use the friend keyword only in the prototype found in the class declaration. You don't use it in the function definition, unless the definition also is the prototype.

Listing 11.10 mytime3.h
// mytime3.h -- Time class with friends
#ifndef MYTIME3_H_
#define MYTIME3_H_
#include <iostream>
using namespace std;

class Time
{
private:
    int hours;
    int minutes;
public:
    Time();
    Time(int h, int m = 0);
    void AddMin(int m);
    void AddHr(int h);
    void Reset(int h = 0, int m = 0);
    Time operator+(const Time & t) const;
    Time operator-(const Time & t) const;
    Time operator*(double n) const;
    friend Time operator*(double m, const Time & t)
           { return t * m; }    // inline definition
    friend ostream & operator<<(ostream & os, const Time & t);
};
#endif

Listing 11.11 shows the revised set of definitions. Note again that the methods use the Time:: qualifier while the friend function does not.

Listing 11.11 mytime3.cpp
// mytime3.cpp  -- implement Time methods
#include "mytime3.h"

Time::Time()
{
    hours = minutes = 0;
}

Time::Time(int h, int m )
{
    hours = h;
    minutes = m;
}

void Time::AddMin(int m)
{
    minutes += m;
    hours += minutes / 60;
    minutes %= 60;
}
void Time::AddHr(int h)
{
    hours += h;
}

void Time::Reset(int h, int m)
{
    hours = h;
    minutes = m;
}

Time Time::operator+(const Time & t) const
{
    Time sum;
    sum.minutes = minutes + t.minutes;
    sum.hours = hours + t.hours + sum.minutes / 60;
    sum.minutes %= 60;
    return sum;
}

Time Time::operator-(const Time & t) const
{
    Time diff;
    int tot1, tot2;
    tot1 = t.minutes + 60 * t.hours;
    tot2 = minutes + 60 * hours;
    diff.minutes = (tot2 - tot1) % 60;
    diff.hours = (tot2 - tot1) / 60;
    return diff;
}

Time Time::operator*(double mult) const
{
    Time result;
    long totalminutes = hours * mult * 60 + minutes * mult;
    result.hours = totalminutes / 60;
    result.minutes = totalminutes % 60;
    return result;
}

ostream & operator<<(ostream & os, const Time & t)
{
    os << t.hours << " hours, " << t.minutes << " minutes";
    return os;
}

Listing 11.12 shows a sample program.

Listing 11.12 usetime3.cpp
// usetime3.cpp -- use fourth draft of Time class
// compile usetime3.cpp and mytime3.cpp together
#include <iostream>
#include "mytime3.h"
using namespace std;

int main()
{
    Time A;
    Time B(5, 40);
    Time C(2, 55);

    cout << "A, B, and C:\n";
    cout << A <<"; " << B << ": " << C << endl;
    A = B + C;     // operator+()
    cout << "B + C: " << A << endl;
    A = B * 2.75;  // member operator*()
    cout << "B * 2.75: " << A << endl;
    cout << "10 * B: " << 10 * B << endl;
    return 0;
}

Here is the output:

A, B, and C:
0 hours, 0 minutes; 5 hours, 40 minutes: 2 hours, 55 minutes
B + C: 8 hours, 35 minutes
B * 2.75: 15 hours, 35 minutes
10 * B: 56 hours, 40 minutes

Overloaded Operators: Member Versus Nonmember Functions

For many operators, you have a choice between using member functions or nonmember functions to implement operator overloading. Typically, the nonmember version would be a friend function so that it can access directly the private data for a class. For example, consider the addition operator for the Time class. It had this prototype in the Time class declaration:

Time operator+(const Time & t) const;                     // member version

Instead, the class could have used the following prototype:

friend Time operator+(const Time & t1, const Time & t2);  // non-member version

The addition operator requires two operands. For the member function version, one is passed implicitly via the this pointer and the second is passed explicitly as a function argument. For the friend version, both are passed as arguments.

Remember

graphics/arrow.gif

A nonmember version of an overloaded operator function requires as many formal parameters as the operator has operands. A member version of the same operator requires one parameter less because one operand is passed implicitly as the invoking object.

Either of these two prototypes matches the expression B + C, where B and C are type Time objects. That is, the compiler can convert the statement

A = B + C;

to either of the following:

A = B.operator+;    // member function
A = operator+(B, C);   // non-member function

Keep in mind that you must choose one or the other form when defining a given operator, but not both. Because both forms match the same expression, defining both forms is considered an ambiguity error.

Which form, then, is it best to use? For some operators, as mentioned earlier, the member function is the only valid choice. Otherwise, it often doesn't make much difference. Some times, depending upon the class design, the nonmember version may have an advantage, particularly if you have defined type conversions for the class. This chapter will show an example later.

More Overloading: A Vector Class

Let's look at another class design that uses operator overloading and friends, a class representing vectors. The class will also illustrate further aspects of class design, such as incorporating two different ways of describing the same thing into an object. Even if you don't care for vectors, you can use many of the new techniques in other contexts. A vector, as the term is used in engineering and physics, is a quantity having both a magnitude (size) and a direction. For example, if you push something, the effect depends on how hard you push (the magnitude) and in what direction you push. A push in one direction can save a tottering vase, whereas a push in the opposite direction can hasten its rush to doom. To fully describe the motion of your car, you should give both the speed (the magnitude) and the direction; arguing with the highway patrol that you were driving under the speed limit carries little weight if you were traveling in the wrong direction. (Immunologists and computer scientists may use the term vector differently; ignore them, at least until Chapter 16, "The string Class and the Standard Template Library," which looks at the computer science version.) The following note tells you more about vectors, but understanding them completely isn't necessary for following the C++ aspects of the examples.

Vectors

graphics/common.gif

You're a worker bee that has discovered a marvelous nectar cache. You rush back to the hive and announce you've found nectar 120 yards away. "Not enough information," buzz the other bees. "You have to tell us the direction, too!" You answer, "It's 30 degrees north of the sun direction." Knowing both the distance (magnitude) and the direction, the other bees rush to the sweet site. Bees know vectors.

Many quantities involve both a magnitude and a direction. The effect of a push, for example, depends on both its strength and direction. Moving an object on a computer screen involves a distance and a direction. You can describe such things using vectors. For example, you can describe moving (displacing) an object on the screen with a vector, which you can visualize as an arrow drawn from the starting position to the final position. The length of the vector is its magnitude, and that describes how far the point has been displaced. The orientation of the arrow describes the direction (see Figure 11.1). A vector representing such a change in position is called a displacement vector.

Figure 11.1. Describing a displacement with a vector.

graphics/11fig01.gif

Now you're Lhanappa, the great mammoth hunter. Scouts report a mammoth herd 14.1 kilometers to the northwest. But, because of a southeast wind, you don't want to approach from the southeast. So you go 10 kilometers west, then 10 kilometers north, approaching the herd from the south. You know these two displacement vectors bring you to the same location as the single 14.1-kilometer vector pointing northwest. Lhanappa, the great mammoth hunter, also knows how to add vectors.

Adding two vectors has a simple geometric interpretation. First, draw one vector. Then draw the second vector starting from the arrow-end of the first vector. Finally, draw a vector from the starting point of the first vector to the end point of the second vector. This third vector represents the sum of the first two (see Figure 11.2). Note that the length of the sum can be less than the sum of the individual lengths.

Figure 11.2. Adding two vectors.

graphics/11fig02.gif

Vectors make a natural choice for operator overloading. First, you can't represent a vector with a single number, so it makes sense to create a class to represent vectors. Second, vectors have analogs to ordinary arithmetic operations such as addition and subtraction. This parallel suggests overloading the corresponding operators so you can use them with vectors.

To keep things simple, we'll implement a two-dimensional vector, such as a screen displacement, instead of a three-dimensional vector, such as might represent movement of a helicopter or a gymnast. You need just two numbers to describe a two-dimensional vector, but you have a choice of what set of two numbers:

The components are a horizontal vector (the x component) and a vertical vector (the y component), which add up to the final vector. For example, you can describe a motion as moving a point 30 units to the right and 40 units up (see Figure 11.3). That motion puts the point at the same spot as moving 50 units at an angle of 53.1? from the horizontal. Therefore, a vector with a magnitude of 50 and an angle of 53.1?is equivalent to a vector having a horizontal component of 30 and a vertical component of 40. What counts with displacement vectors is where you start and where you end up, not the exact route taken to get there. This choice of representation is basically the same thing we covered with the Chapter 7 program that converted between rectangular and polar coordinates.

Figure 11.3. x and y components of a vector.

graphics/11fig03.gif

Sometimes one form is more convenient, sometimes the other, so we'll incorporate both representations into the class description. See the note on Multiple Representations and Classes coming up shortly. Also, we'll design the class so that if you alter one representation of a vector, the object automatically updates the other representation. The ability to build such intelligence into an object is another C++ class virtue. Listing 11.13 presents a class declaration. To refresh your memory about namespaces, the listing places the class declaration inside the VECTOR namespace.

Compatibility Note

graphics/hands.gif

If your system does not support namespaces, you can remove the following lines:

namespace VECTOR
{

and

}    // end namespace VECTOR
Listing 11.13 vector.h
// vect.h -- Vector class with <<, mode state
#ifndef VECTOR_H_
#define VECTOR_H_
namespace VECTOR
{

    class Vector
    {
    private:
        double x;          // horizontal value
        double y;          // vertical value
        double mag;        // length of vector
        double ang;        // direction of vector
        char mode;         // 'r' = rectangular, 'p' = polar
    // private methods for setting values
        void set_mag();
        void set_ang();
        void set_x();
        void set_y();
    public:
        Vector();
        Vector(double n1, double n2, char form = 'r');
        void set(double n1, double n2, char form = 'r');
        ~Vector();
        double xval() const {return x;}        // report x value
        double yval() const {return y;}        // report y value
        double magval() const {return mag;}    // report magnitude
        double angval() const {return ang;}    // report angle
        void polar_mode();                    // set mode to 'p'
        void rect_mode();                     // set mode to 'r'
    // operator overloading
        Vector operator+(const Vector & b) const;
        Vector operator-(const Vector & b) const;
        Vector operator-() const;
        Vector operator*(double n) const;
    // friends
        friend Vector operator*(double n, const Vector & a);
        friend ostream & operator<<(ostream & os, const Vector & v);
    };
}    // end namespace VECTOR
#endif

Notice that the four functions that report component values are defined in the class declaration. This automatically makes them inline functions. The fact that these functions are so short makes them excellent candidates for inlining. None of them should alter object data, so they are declared using the const modifier. As you may recall from Chapter 10, this is the syntax for declaring a function that doesn't modify the object it implicitly accesses.

Listing 11.14 shows all the methods and friend functions declared in Listing 11.13. The listing uses the open nature of namespaces to add the method definitions to the VECTOR namespace. Note how the constructor functions and the set() function each set both the rectangular and the polar representations of the vector. Thus, either set of values is available immediately without further calculation should you need them. Also, as mentioned in Chapters 4, "Compound Types," and 7, C++'s built-in math functions use angles in radians, so the functions built conversion to and from degrees into the methods. The implementation hides such things as converting from polar coordinates to rectangular coordinates or converting radians to degrees from the user. All the user needs to know is that the class uses angles in degrees and that it makes a vector available in two equivalent representations.

These design decisions follow the OOP tradition of having the class interface concentrate on the essentials (the abstract model) while hiding the details. Thus, when you use the Vector class, you can think about a vector's general features, such as that they can represent displacements and that you can add two vectors. Whether you express a vector in component notation or in magnitude, direction notation becomes secondary, for you can set a vector's values and display them in whichever format is most convenient at the time.

We'll look at some of the features in more detail next.

Compatibility Note

graphics/hands.gif

Some systems may still use math.h instead of cmath. Also, some C++ systems don't automatically search the math library. For example, some UNIX systems require that you do the following:

$ CC source_file(s) -lm

The -lm option instructs the linker to search the math library. So, when you eventually compile programs using the Vector class, if you get a message about undefined externals, try the -lm option or check to see if your system requires something similar.

As with the header file, if your system does not support namespaces, you can remove the following lines:

namespace VECTOR
{

and
}    // end namespace VECTOR
Listing 11.14 vector.cpp
// vect.cpp -- methods for Vector class
#include <iostream>
#include <cmath>
using namespace std;
#include "vect.h"

namespace VECTOR
{
    const double Rad_to_deg = 57.2957795130823;

    // private methods
    // calculates magnitude from x and y
    void Vector::set_mag()
    {
        mag = sqrt(x * x + y * y);
    }

    void Vector::set_ang()
    {
        if (x == 0.0 && y == 0.0)
            ang = 0.0;
        else
            ang = atan2(y, x);
    }
    // set x from polar coordinate
    void Vector::set_x()
    {
        x = mag * cos(ang);
    }

    // set y from polar coordinate
    void Vector::set_y()
    {
        y = mag * sin(ang);
    }

    // public methods
    Vector::Vector()             // default constructor
    {
        x = y = mag = ang = 0.0;
        mode = 'r';
    }

    // construct vector from rectangular coordinates if form is r
    // (the default) or else from polar coordinates if form is p
    Vector::Vector(double n1, double n2, char form)
    {
        mode = form;
        if (form == 'r')
         {
             x = n1;
             y = n2;
             set_mag();
             set_ang();
        }
        else if (form == 'p')
        {
             mag = n1;
             ang = n2 / Rad_to_deg;
             set_x();
             set_y();
        }
        else
        {
             cout << "Incorrect 3rd argument to Vector() -- ";
             cout << "vector set to 0\n";
             x = y = mag = ang = 0.0;
             mode = 'r';
        }
    }

    // set vector from rectangular coordinates if form is r (the
    // default) or else from polar coordinates if form is p
    void Vector:: set(double n1, double n2, char form)
    {
        mode = form;
        if (form == 'r')
         {
             x = n1;
             y = n2;
             set_mag();
             set_ang();
        }
        else if (form == 'p')
        {
             mag = n1;
             ang = n2 / Rad_to_deg;
             set_x();
             set_y();
        }
        else
        {
             cout << "Incorrect 3rd argument to Vector() -- ";
             cout << "vector set to 0\n";
             x = y = mag = ang = 0.0;
             mode = 'r';
        }
    }

    Vector::~Vector()    // destructor
    {
    }

    void Vector::polar_mode()    // set to polar mode
    {
        mode = 'p';
    }

    void Vector::rect_mode()     // set to rectangular mode
    {
        mode = 'r';
    }

    // operator overloading
    // add two Vectors
    Vector Vector::operator+(const Vector & b) const
    {
        return Vector(x + b.x, y + b.y);
    }

    // subtract Vector b from a
    Vector Vector::operator-(const Vector & b) const
    {
        return Vector(x - b.x, y - b.y);
    }
    // reverse sign of Vector
    Vector Vector::operator-() const
    {
        return Vector(-x, -y);
    }

    // multiple vector by n
    Vector Vector::operator*(double n) const
    {
        return Vector(n * x, n * y);
    }

    // friend methods
    // multiply n by Vector a
    Vector operator*(double n, const Vector & a)
    {
        return a * n;
    }

    // display rectangular coordinates if mode is r,
    // else display polar coordinates if mode is p
    ostream & operator<<(ostream & os, const Vector & v)
    {
        if (v.mode == 'r')
             os << "(x,y) = (" << v.x << ", " << v.y << ")";
        else if (v.mode == 'p')
        {
             os << "(m,a) = (" << v.mag << ", "
                 << v.ang * Rad_to_deg << ")";
        }
        else
             os << "Vector object mode is invalid";
        return os;
    }

}   // end namespace VECTOR

Using a State Member

The class stores both the rectangular coordinates and the polar coordinates for a vector. It uses a member called mode to control which form the constructor, the set() method, and the overloaded operator<<() function use, with 'r' representing the rectangular mode (the default) and 'p' the polar mode. Such a member is termed a state member because it describes the state an object is in. To see what this means, look at the code for the constructor:

Vector::Vector(double n1, double n2, char form)
{
    mode = form;
    if (form == 'r')
     {
         x = n1;
         y = n2;
         set_mag();
         set_ang();
    }
    else if (form == 'p')
    {
         mag = n1;
         ang = n2 / Rad_to_deg;
         set_x();
         set_y();
    }
    else
    {
         cout << "Incorrect 3rd argument to Vector() -- ";
         cout << "vector set to 0\n";
         x = y = mag = ang = 0.0;
         mode = 'r';
    }
}

If the third argument is 'r' or if it is omitted (the prototype assigns a default value of 'r'), the inputs are interpreted as rectangular coordinates, whereas a value of 'p' causes them to be interpreted as polar coordinates:

Vector folly(3.0, 4.0);            // set x = 3, y = 4
Vector foolery(20.0, 30.0, 'p');   // set mag = 20, ang = 30

Note that the constructor uses the private methods set_mag() and set_ang() to set the magnitude and angle values if you provide x and y values, and it uses the private set_x() and set_y() methods to set x and y values if you provide magnitude and angle values. Also note that the constructor delivers a warning message and sets the state to 'r' if something other than 'r' or 'p' is specified.

Similarly, the operator<<() function uses the mode to determine how values are displayed:

// display rectangular coordinates if mode is r,
// else display polar coordinates if mode is p
ostream & operator<<(ostream & os, const Vector & v)
{
    if (v.mode == 'r')
         os << "(x,y) = (" << v.x << ", " << v.y << ")";
    else if (v.mode == 'p')
    {
         os << "(m,a) = (" << v.mag << ", "
             << v.ang * Rad_to_deg << ")";
    }
    else
         os << "Vector object mode is invalid";
    return os;
}

The various methods that can set the mode are careful to accept only 'r' and 'p' as valid values, so the final else in this function never should be reached. Still, it's often a good idea to check; such a check can help catch an otherwise obscure programming error.

Multiple Representations and Classes

graphics/common.gif

Quantities having different, but equivalent, representations are common. For example, you can measure gasoline consumption in miles per gallon, as done in the United States, or in liters per 100 kilometers, as done in Europe. You can represent a number in string form or numeric form, and you can represent intelligence as an IQ or in kiloturkeys. Classes lend themselves nicely to encompassing different aspects and representations of an entity in a single object. First, you can store multiple representations in one object. Second, you can write the class functions so that assigning values for one representation automatically assigns values for the other representation(s). For example, the set_by_polar() method for the Vector class sets the mag and ang members to the function arguments, but it also sets the x and y members. By handling conversions internally, a class can help you think of a quantity in terms of its essential nature rather than in terms of its representation.

More Overloading

Adding two vectors is very simple when you use x,y coordinates. Just add the two x components to get the x component of the answer and add the two y components to get the y component of the answer. From this description, you might be tempted to use this code:

Vector Vector::operator+(const Vector & b) const
{
    Vector sum;
    sum.x = x + b.x;
    sum.y = y + b.y;
    return sum;
}

And this would be fine if the object stored only the x and y components. Unfortunately, this version of the code fails to set the polar values. This could be fixed by adding more code:

Vector Vector::operator+(const Vector & b) const
{
    Vector sum;
    sum.x = x + b.x;
    sum.y = y + b.y;
    sum.set_ang(sum.x, sum.y);
    sum.set_mag(sum.x, sum.y);
    return sum;
}

But it is much simpler and more reliable to let a constructor do the work:

Vector Vector::operator+(const Vector & b) const
{
    return Vector(x + b.x, y + b.y);      // return the constructed Vector
}

Here, the code hands the Vector constructor the two new values for the x and y components. The constructor then creates a nameless new object using these values, and the function returns a copy of that object. This way, you guarantee that the new Vector object is created according to the standard rules you lay down in the constructor.

Tip

graphics/bulb.gif

If a method needs to compute a new class object, see if you can use a class constructor to do the work. Not only does that save you trouble, it ensures that the new object is constructed in the proper fashion.

Multiplication

In visual terms, multiplying a vector by a number makes the vector longer or shorter by that factor. So multiplying a vector by 3 produces a vector with three times the length, but still pointed in the same direction. It's easy to translate that image into how the class represents a vector. In polar terms, multiply the magnitude and leave the angle alone. In rectangular terms, you multiply a vector by a number by multiplying its x and y components separately by the number. That is, if a vector has components of 5 and 12, multiplying by 3 makes the components 15 and 36. And that is what the overloaded multiplication operator does:

Vector Vector::operator*(double n) const
{
    return Vector(n * x, n * y);
}

As with overloaded addition, the code lets a constructor create the correct Vector from the new x and y components. This handles multiplying a Vector value times a double value. Just as in the Time example, we can use an inline friend function to handle double times Vector:

Vector operator*(double n, const Vector & a)  // friend function
{
    return a * n;   // convert double times Vector to Vector times double
}
More Refinement: Overloading an Overloaded Operator

In ordinary C++, the - operator already has two meanings. First, when used with two operands, it's the subtraction operator. The subtraction operator is termed a binary operator because it has exactly two operands. Second, when used with one operand, as in -x, it's a minus sign operator. This form is termed a unary operator, meaning it has exactly one operand. Both operations梥ubtraction and sign reversal梞ake sense for vectors, too, so the Vector class has both.

To subtract vector B from vector A, you simply subtract components, so the definition for overloading subtraction is quite similar to the one for addition:

Vector operator-(const Vector & b) const;            // prototype
Vector Vector::operator-(const Vector & b) const     // definition
{
    return Vector(x - b.x, y - b.y);  // return the constructed Vector
}

Here, it's important to get the correct order. Consider the following statement:

diff = v1 - v2;

It's converted to a member function call:

diff = v1.operator-(v2);

This means the vector passed as the explicit argument is subtracted from the implicit vector argument, so we should use x - b.x and not b.x - x.

Next, consider the unary minus operator, which takes just one operand. Applying this operator to a regular number, as in -x, changes the sign of the value. Thus, applying this operator to a vector reverses the sign of each component. More precisely, the function should return a new vector that is the reverse of the original. (In polar terms, negation leaves the magnitude unchanged, but reverses the direction. Many politicians with little or no mathematical training, nonetheless, have an intuitive mastery of this operation.) Here are the prototype and definition for overloading negation:

Vector operator-() const;
Vector Vector::operator-() const
{
    return Vector (-x, -y);
}

Note that now there are two separate definitions for operator-(). That's fine, because the two definitions have different signatures. You can define both binary and unary versions of the - operator because C++ provides both binary and unary versions of that operator to begin with. An operator having only a binary form, such as division (/), can only be overloaded as a binary operator.

Remember

graphics/arrow.gif

Because operator overloading is implemented with functions, you can overload the same operator many times, as long as each operator function has a distinct signature and as long as each operator function has the same number of operands as the corresponding built-in C++ operator.

An Implementation Comment

This implementation stores both rectangular and polar coordinates for a vector in the object. However, the public interface doesn't depend upon this fact. All the interface calls for is that both representations can be displayed and that individual values can be returned. The internal implementation could be quite different. For example, the object could store just the x and y components. Then, say, the magval() method, which returns the value of the magnitude of the vector, could calculate the magnitude from the x and y values instead of just looking up the value as stored in the object. Such an approach changes the implementation, but leaves the user interface unchanged. This separation of interface from implementation is one of the goals of OOP. It lets you fine-tune an implementation without changing the code in programs that use the class.

Both of these implementations have advantages and disadvantages. Storing the data means the object occupies more memory and that code has to be careful to update both rectangular and polar representations each time a Vector object is changed. But data look-up is faster. If an application often had need to access both representations of a vector, the implementation used in this example would be preferable; if polar data were needed only infrequently, the other implementation would be better. You could choose to use one implementation in one program and the second implementation in another, yet retain the same user interface for both.

Taking the Vector Class on a Random Walk

Listing 11.15 provides a short program using the revised class. It simulates the famous Drunkard's Walk problem. Actually, now that the drunk is recognized as someone with a serious health problem rather than as a source of amusement, it's usually called the Random Walk problem. The idea is that you place someone at a lamp post. The subject begins walking, but the direction of each step varies randomly from the preceding step. One way of phrasing the problem is, how many steps does it take the random walker to travel, say, 50 feet away from the post. In terms of vectors, this amounts to adding a bunch of randomly oriented vectors until the sum exceeds 50 feet.

Listing 11.15 lets you select the target distance to be traveled and the length of the wanderer's step. It maintains a running total representing the position after each step (represented as a vector), and reports the number of steps needed to reach the target distance along with the walker's location (in both formats). As you'll see, the walker's progress is quite inefficient. A journey of a thousand steps, each two feet long, may carry the walker only 50 feet from the starting point. The program divides the net distance traveled (50 feet in this case) by the number of steps to provide a measure of the walker's inefficiency. All the random direction changes make this average much smaller than the length of a single step. To select directions randomly, the program uses the standard library functions rand(), srand(), and time(), described in the Program Notes. Remember to compile Listing 11.14 along with Listing 11.15.

Listing 11.15 randwalk.cpp
// randwalk.cpp -- use the Vector class
// compile with the vect.cpp file
#include <iostream>
#include <cstdlib>      // rand(), srand() prototypes
#include <ctime>        // time() prototype
using namespace std;
#include "vect.h"
int main()
{
    using VECTOR::Vector;
    srand(time(0));     // seed random-number generator
    double direction;
    Vector step;
    Vector result(0.0, 0.0);
    unsigned long steps = 0;
    double target;
    double dstep;
    cout << "Enter target distance (q to quit): ";
    while (cin >> target)
    {
        cout << "Enter step length: ";
        if (!(cin >> dstep))
            break;
        while (result.magval() < target)
        {
            direction = rand() % 360;
            step.set(dstep, direction, 'p');
            result = result + step;
            steps++;
        }
        cout << "After " << steps << " steps, the subject "
            "has the following location:\n";
        cout << result << "\n";
        result.polar_mode();
        cout << " or\n" << result << "\n";
        cout << "Average outward distance per step = "
            << result.magval()/steps << "\n";
        steps = 0;
        result.set(0.0, 0.0);
        cout << "Enter target distance (q to quit): ";
    }
    cout << "Bye!\n";
    return 0;
}

Compatibility Note

graphics/hands.gif

You might have to use stdlib.h instead of cstdlib and time.h instead of ctime. If you system doesn't support namespaces, omit the following line:

using VECTOR::Vector;

Here is a sample run:

Enter target distance (q to quit): 50
Enter step length: 2
After 253 steps, the subject has the following location:
(x,y) = (46.1512, 20.4902)
 or
 (m,a) = (50.495, 23.9402)
Average outward distance per step = 0.199587
Enter target distance (q to quit): 50
Enter step length: 2
After 951 steps, the subject has the following location:
(x,y) = (-21.9577, 45.3019)
 or
(m,a) = (50.3429, 115.8593)
Average outward distance per step = 0.0529362
Enter target distance (q to quit): 50
Enter step length: 1
After 1716 steps, the subject has the following location:
(x,y) = (40.0164, 31.1244)
 or
(m,a) = (50.6956, 37.8755)
Average outward distance per step = 0.0295429
Enter target distance (q to quit): q
Bye!

The random nature of the process produces considerable variation from trial to trial, even if the initial conditions are the same. On the average, however, halving the step size quadruples the number of steps needed to cover a given distance. Probability theory suggests that, on the average, the number of steps (N) of length s needed to reach a net distance of D is given by the following equation:

N = (D/s)2

This is just an average, but there will be considerable variations from trial to trial. For example, a thousand trials of attempting to travel 50 feet in 2-foot steps yielded an average of 636 steps (close to the theoretical value of 625) to travel that far, but the range was from 91 to 3951. Similarly, a thousand trials of traveling 50 feet in 1-foot steps averaged 2557 steps (close to the theoretical value of 2500) with a range of 345 to 10882. So if you find yourself walking randomly, be confident and take long steps. You still won't have any control over the direction you wind up going, but at least you'll get farther.

Program Notes

First, let's note how painless it was to use the VECTOR namespace. The using-declaration

using VECTOR::Vector;

placed the name of the Vector class in scope. Because all the Vector class methods have class scope, importing the class name also made the Vector methods available without the need of any further using-declarations.

Next, let's talk about random numbers. The standard ANSI C library, which also comes with C++, includes a rand() function that returns a random integer in the range from 0 to some implementation-dependent value. Our program uses the modulus operator to get an angle value in the range 0?tt>359. The rand() function works by applying an algorithm to an initial seed value to get a random value. That value is used as the seed for the next function call, and so on. The numbers are really pseudorandom, for ten consecutive calls normally produce the same set of ten random numbers. (The exact values will depend on the implementation.) However, the srand() function lets you override the default seed value and initiate a different sequence of random numbers. This program uses the return value of time(0) to set the seed. The time(0) function returns the current calendar time, often implemented as the number of seconds since some specific date. (More generally, time() takes the address of a type time_t variable and puts the time into that variable and also returns it. Using 0 for the address argument obviates the need for an otherwise unneeded time_t variable.) Thus, the statement

srand(time(0));

sets a different seed each time you run the program, making the random output appear even more random. The cstdlib header file (formerly stdlib.h) contains the prototypes for srand() and rand(), whereas ctime (formerly time.h) contains the time() prototype.

The program uses the result vector to keep track of the walker's progress. Each cycle of the inner loop, the program sets the step vector to a new direction and adds it to the current result vector. When the magnitude of result exceeds the target distance, the loop terminates.

By setting the vector mode, the program displays the final position in rectangular terms and in polar terms.

Incidentally, the statement

result = result + step;

has the effect of placing result in the 'r' mode regardless of the initial modes of result and step. Here's why. First, the addition operator function creates and returns a new vector holding the sum of the two arguments. The function creates that vector using the default constructor, which creates vectors in the 'r' mode. Thus, the vector being assigned to result is in the 'r' mode. By default, assignment assigns each member variable individually, so 'r' is assigned to result.mode. If you would prefer some other behavior, such as result retaining its original mode, you can override default assignment by defining an assignment operator for the class. The next chapter shows examples of this.

Automatic Conversions and Type Casts for Classes

The next topic on the class menu is type conversion. We'll look into how C++ handles conversions to and from user-defined types. To set the stage, let's first review how C++ handles conversions for its built-in types. When you make a statement assigning a value of one standard type to a variable of another standard type, C++ automatically will convert the value to the same type as the receiving variable, providing the two types are compatible. For example, the following statements all generate numeric type conversions:

long count = 8;      // int value 8 converted to type long
double time = 11;    // int value 11 converted to type double
int side = 3.33;     // double value 3.33 converted to type int 3

These assignments work because C++ recognizes that the diverse numeric types all represent the same basic thing, a number, and because C++ incorporates built-in rules for making the conversions. Recall, however (from your reading of Chapter 3, "Dealing with Data"), that you can lose some precision in these conversions. For example, assigning 3.33 to the int variable that side results in that side getting the value 3, losing the 0.33 part.

The C++ language does not automatically convert types that are not compatible. For example, the statement

int * p = 10;  // type clash

fails because the left-hand side is a pointer-type, whereas the right-hand side is a number. And even though a computer may represent an address internally with an integer, integers and pointers conceptually are quite different. For example, you wouldn't square a pointer. However, when automatic conversions fail, you may use a type cast:

int * p = (int *) 10;  // ok, p and (int *) 10 both pointers

This sets a pointer to the address 10 by type casting 10 to type pointer-to-int (that is, type int *).

You may define a class sufficiently related to a basic type or to another class that it makes sense to convert from one form to another. In that case, you can instruct C++ how to make such conversions automatically or, perhaps, via a type cast. To show how that works, let's recast the pounds-to-stone program from Chapter 3 into class form. First, design an appropriate type. Fundamentally, we're representing one thing (a weight) two ways (pounds and stone). A class provides an excellent way to incorporate two representations of one concept into a single entity. Therefore, it makes sense to place both representations of weight into the same class and then to provide class methods for expressing the weight in different forms. Listing 11.16 provides the class header.

Listing 11.16 stonewt.h
// stonewt.h -- definition for Stonewt class
#ifndef STONEWT_H_
#define STONEWT_H_
class Stonewt
{
private:
    enum {Lbs_per_stn = 14};      // pounds per stone
    int stone;                    // whole stones
    double pds_left;              // fractional pounds
    double pounds;                // entire weight in pounds
public:
    Stonewt(double lbs);          // constructor for double pounds
    Stonewt(int stn, double lbs); // constructor for stone, lbs
    Stonewt();                    // default constructor
    ~Stonewt();
    void show_lbs() const;        // show weight in pounds format
    void show_stn() const;        // show weight in stone format
};
#endif

As mentioned in Chapter 10, enum provides a convenient way to define class-specific constants, providing that they are integers. New compilers allow the following alternative:

static const int Lbs_per_stn = 14;

Note that the class has three constructors. They allow you to initialize a Stonewt object to a floating-point number of pounds, or to a stone and pound combination. Or you can create a Stonewt object without initializing it:

Stonewt kindy(118);  // weight = 118 pounds
Stonewt kandy(8, 2); // weight = 8 stone, 2 pounds
Stonewt kendy;       // weight = default value

Also, the class provides two display functions. One displays the weight in pounds, and the other displays the weight in stone and pounds. Listing 11.17 shows the class methods implementation. Note that each constructor assigns values to all three private members. Thus, creating a Stonewt object automatically sets both representations of weight.

Listing 11.17 stonewt.cpp
// stonewt.cpp -- Stonewt methods
#include <iostream>
using namespace std;
#include "stonewt.h"

// construct Stonewt object from double value
Stonewt::Stonewt(double lbs)
{
    stone = int (lbs) / Lbs_per_stn;    // integer division
    pds_left = int (lbs) % Lbs_per_stn + lbs - int(lbs);
    pounds = lbs;
}

// construct Stonewt object from stone, double values
Stonewt::Stonewt(int stn, double lbs)
{
    stone = stn;
    pds_left = lbs;
    pounds =  stn * Lbs_per_stn +lbs;
}

Stonewt::Stonewt()          // default constructor, wt = 0
{
    stone = pounds = pds_left = 0;
}
Stonewt::~Stonewt()         // destructor
{
}

// show weight in stones
void Stonewt::show_stn() const
{
    cout << stone << " stone, " << pds_left << " pounds\n";
}

// show weight in pounds
void Stonewt::show_lbs() const
{
    cout << pounds << " pounds\n";
}

Because a Stonewt object represents a single weight, it makes sense to provide ways to convert an integer or a floating-point value to a Stonewt object. And we have already done so! In C++, any constructor taking a single argument acts as a blueprint for converting a value of that argument type to the class type. Thus the constructor

Stonewt(double lbs);  // template for double-to-Stonewt conversion

serves as instructions for converting a type double value to a type Stonewt value. That is, you can write code like the following:

Stonewt myCat;        // create a Stonewt object
myCat = 19.6;         // use Stonewt(double) to convert 19.6 to Stonewt

The program will use the Stonewt(double) constructor to construct a temporary Stonewt object, using 19.6 as the initialization value. Then memberwise assignment will copy the contents of the temporary object into myCat. This process is termed an implicit conversion because it happens automatically without the need of an explicit type cast.

Only a constructor that can be used with just one argument works as a conversion function. The constructor

Stonewt(int stn, double lbs);

has two arguments, so it cannot be used to convert types.

Having a constructor work as an automatic type-conversion function seems like a nice feature. As programmers acquired more experience working with C++, however, they found that the automatic aspect isn't always desirable, for it can lead to unexpected conversions. So recent C++ implementations have a new keyword, explicit, to turn off the automatic aspect. That is, you can declare the constructor this way:

explicit Stonewt(double lbs);   // no implicit conversions allowed

This turns off implicit conversions such as the example above but still allows explicit conversions; that is, conversions using explicit type casts:

Stonewt myCat;          // create a Stonewt object
myCat = 19.6;           // not valid if Stonewt(double) is declared as explicit
mycat = Stonewt(19.6);  // ok, an explicit conversion
mycat = (Stonewt) 19.6; // ok, old form for explicit typecast

Remember

graphics/arrow.gif

A C++ constructor containing one argument defines a type conversion from the argument type to the class type. If the constructor is qualified with the keyword explicit, the constructor is used for explicit conversions only; otherwise, it also is used for implicit conversions.

When will the compiler use the Stonewt(double) function? If the keyword explicit is used in the declaration, Stonewt(double) will be used only for an explicit type cast; otherwise, it also will be used for the following implicit conversions.

Let's look at the last point in more detail. The argument-matching process provided by function prototyping will let the Stonewt(double) constructor act as conversions for other numerical types. That is, both of the following statements work by first converting int to double and then using the Stonewt(double) constructor.

Stonewt Jumbo(7000);   // uses Stonewt(double), converting int to double
Jumbo = 7300;          // uses Stonewt(double), converting int to double

However, this two-step conversion process works only if there is an unambiguous choice. That is, if the class also defined a Stonewt(long) constructor, the compiler would reject these statements, probably pointing out that an int can be converted to either a long or a double, so the call is ambiguous.

Listing 11.18 uses the class constructors to initialize some Stonewt objects and to handle type conversions. Remember to compile Listing 11.17 along with Listing 11.18.

Listing 11.18 stone.cpp
// stone.cpp -- user-defined conversions
// compile with stonewt.cpp
#include <iostream>
using namespace std;
#include "stonewt.h"
void display(Stonewt st, int n);
int main()
{
    Stonewt pavarotti = 260; // uses constructor to initialize
    Stonewt wolfe(285.7);    // same as Stonewt wolfe = 285.7;
    Stonewt taft(21, 8);

    cout << "The tenor weighed ";
    pavarotti.show_stn();
    cout << "The detective weighed ";
    wolfe.show_stn();
    cout << "The President weighed ";
    taft.show_lbs();
    pavarotti = 265.8;       // uses constructor for conversion
    taft = 325;             // same as taft = Stonewt(325);
    cout << "After dinner, t he tenor weighed ";
    pavarotti.show_stn();
    cout << "After dinner, the President weighed ";
    taft.show_lbs();
    display(taft, 2);
    cout << "The wrestler weighed even more.\n";
    display(422, 2);
    cout << "No stone left unearned\n";
    return 0;
}

void display(Stonewt st, int n)
{
    for (int i = 0; i < n; i++)
    {
        cout << "Wow! ";
        st.show_stn();
    }
}

Here is the output:

The tenor weighed 18 stone, 8 pounds
The detective weighed 20 stone, 5.7 pounds
The President weighed 302 pounds
After dinner, the tenor weighed 18 stone, 13.8 pounds
After dinner, the President weighed 325 pounds
Wow! 23 stone, 3 pounds
Wow! 23 stone, 3 pounds
The wrestler weighed even more.
Wow! 30 stone, 2 pounds
Wow! 30 stone, 2 pounds
No stone left unearned

Program Notes

First, note that when a constructor has a single argument, you can use the following form when initializing a class object:

// a syntax for initializing a class object when
// using a constructor with one argument
Stonewt pavarotti = 260;

This is equivalent to the other two forms we've used:

// standard syntax forms for initializing class objects
Stonewt pavarotti(260);
Stonewt pavarotti = Stonewt(260);

However, the last two forms can also be used with constructors having multiple arguments.

Next, note the following two assignments from Listing 11.18:

pavarotti = 265.8;
taft = 325;

The first assignment uses the constructor with a type double argument to convert 265.8 to a type Stonewt value. This sets the pounds member of pavarotti to 265.8. Because it uses the constructor, this assignment also sets the stone and pds_left members of the class. Similarly, the second assignment converts a type int value to type double and then uses Stonewt(double) to set all three member values in the process.

Finally, note the following function call:

display(422, 2);    // convert 422 to double, then to Stonewt

The prototype for display() indicates that its first argument should be the Stonewt object. Confronted with an int argument, the compiler looks for a Stonewt(int) constructor to convert the int to the desired Stonewt type. Failing to find that constructor, the compiler looks for a constructor with some other built-in type to which an int can be converted. The Stonewt(double) constructor fits the bill. So the compiler converts int to double and then uses Stonewt(double) to convert the result to a Stonewt object.

Conversion Functions

Listing 11.18 converts a number to a Stonewt object. Can you do the reverse? That is, can you convert a Stonewt object to a double value, as in the following?

Stonewt wolfe(285.7);
double host = wolfe;  // ?? possible ??

The answer is that you can do this, but not by using constructors. Constructors only provide for converting another type to the class type. To do the reverse, you have to use a special form of C++ operator function called a conversion function.

Conversion functions are user-defined type casts, and you can use them the way you would use a type cast. For example, if you define a Stonewt-to-double conversion function, you can use the following conversions:

Stonewt wolfe(285.7);
double host = double (wolfe);      // syntax #1
double thinker = (double) wolfe;   // syntax #2

Or you can let the compiler figure out what to do:

Stonewt wells(20, 3);
double star = wells;   // implicit use of conversion function

The compiler, noting that the right-hand side is type Stonewt and the left-hand side is type double, looks to see if you've defined a conversion function matching that description. (If it can't find such a definition, the compiler generates an error message to the effect that it can't assign a Stonewt to a double.)

So how do you create a conversion function? To convert to type typeName, use a conversion function of this form:

operator typeName();

Note the following points:

For example, a function to convert to type double would have this prototype:

operator double();

The typeName part tells the conversion the type to which to convert, so no return type is needed. The fact that the function is a class method means it has to be invoked by a particular class object, and that tells the function which value to convert. Thus, the function doesn't need arguments.

To add functions converting stone_wt objects to type int and to type double, then, requires adding the following prototypes to the class declaration:

operator int();
operator double();

Listing 11.19 shows the modified class declaration.

Listing 11.19 stonewt1.h
// stonewt1.h -- revised definition for Stonewt class
#ifndef STONEWT1_H_
#define STONEWT1_H_
class Stonewt
{
private:
    enum {Lbs_per_stn = 14};      // pounds per stone
int stone;                        // whole stones
    double pds_left;              // fractional pounds
    double pounds;                // entire weight in pounds
public:
    Stonewt(double lbs);          // construct from double pounds
    Stonewt(int stn, double lbs); // construct from stone, lbs
    Stonewt();                    // default constructor
    ~Stonewt();
    void show_lbs() const;        // show weight in pounds format
    void show_stn() const;        // show weight in stone format
// conversion functions
    operator int() const;
    operator double() const;
};
#endif

Next, Listing 11.20 shows the definitions for these two conversion functions; these definitions should be added to the class member function file. Note that each function does return the desired value even though there is no declared return type. Also note the int conversion definition rounds to the nearest integer rather than truncating. For example, if pounds is 114.4, then pounds + 0.5 is 114.9, and int (114.9) is 114. But if pounds is 114.6, then pounds + 0.5 is 115.1, and int (115.1) is 115.

Listing 11.20 stonewt1.cpp
// stonewt1.cpp -- Stonewt class methods + conversion functions
#include <iostream>
using namespace std;
#include "stonewt1.h"

// previous definitions go here

// conversion functions
Stonewt::operator int() const
{

    return int (pounds + 0.5);

}

Stonewt::operator double()const
{
    return pounds;
}

Listing 11.21 tests the new conversion functions. The assignment statement in the program uses an implicit conversion, whereas the final cout statement uses an explicit type cast. Remember to compile Listing 11.20 along with Listing 11.21.

Listing 11.21 stone1.cpp
// stone1.cpp -- user-defined conversion functions
// compile with stonewt1.cpp
#include <iostream>
using namespace std;
#include "stonewt1.h"

int main()
{
    Stonewt poppins(9,2.8);     // 9 stone, 2.8 pounds
    double p_wt = poppins;      // implicit conversion
    cout << "Convert to double => ";
    cout << "Poppins: " << p_wt << " pounds.\n";
    cout << "Convert to int => ";
    cout << "Poppins: " << int (poppins) << " pounds.\n";
    return 0;
}

Here's the program output; it shows the result of converting the type Stonewt object to type double and to type int:

Convert to double => Poppins: 128.8 pounds.
Convert to int => Poppins: 129 pounds.
Applying Type Conversions Automatically

The last example used int (poppins) with cout. Suppose, instead, it omitted the explicit type cast:

cout << "Poppins: " << poppins << " pounds.\n";

Would the program use an implicit conversion, as it did in the following statement?

double p_wt = poppins;

The answer is no. In the p_wt example, the context indicates that poppins should be converted to type double. But in the cout example, nothing indicates whether the conversion should be to int or to double. Facing this lack of information, the compiler would complain that you were using an ambiguous conversion. Nothing in the statement indicates what type to use.

Interestingly, if the class had defined only the double conversion function, the compiler would accept our statement. That's because with only one conversion possible, there is no ambiguity.

You can have a similar situation with assignment. With the current class declarations, the compiler rejects the following statement as ambiguous.

long gone = poppins;   // ambiguous

In C++, you can assign both int and double values to a long variable, so the compiler legitimately can use either conversion function. The compiler doesn't want the responsibility of choosing which. But if you eliminate one of the two conversion functions, the compiler accepts the statement. For example, suppose you omit the double definition. Then the compiler will use the int conversion to convert poppins to a type int value. Then it converts the int value to type long when assigning it to gone.

When the class defines two or more conversions, you can still use an explicit type cast to indicate which conversion function to use. You can use either type cast notation:

long gone = (double) poppins;  // use double conversion
long gone = int (poppins);     // use int conversion

The first statement converts poppins weight to a double value, and then assignment converts the double value to type long. Similarly, the second statement converts poppins first to type int, and then to long.

Like conversion constructors, conversion functions can be a mixed blessing. The problem with providing functions that make automatic, implicit conversions is that they may make conversions when you don't expect them. Suppose, for example, you happen to write the following sleep-deprived code:

int ar[20];
...
Stonewt temp(14, 4);
...
int Temp = 1;
...
cout << ar[temp] << "!\n";  // used temp instead of Temp

Normally, you'd expect the compiler to catch a blunder such as using an object instead of an integer as an array index. But the Stonewt class defines an operator int(), so the Stonewt object temp will be converted to the int 200 and be used as an array index. The moral is that often it's better to use explicit conversions and exclude the possibility of implicit conversions. The keyword explicit doesn't work with conversion functions, but all you have to do is replace a conversion function with a nonconversion function that does the same task, but only if called explicitly. That is, you can replace

Stonewt::operator int() { return int (pounds + 0.5); }

with

int Stonewt::Stone_to_Int() { return int (pounds + 0.5); }

This will disallow

int plb = poppins;

but, if you really need a conversion, allow the following:

int plb = poppins.Stone_to_Int();

Caution

graphics/tnt.gif

Use implicit conversion functions with care. Often a function that can only be invoked explicitly is the better choice.

In summary, then, C++ provides the following type conversions for classes:

Conversions and Friends

Let's bring addition to the Stonewt class. As we mentioned when discussing the Time class, you can use either a member function or a friend function to overload addition. (To simplify matters, assume that no conversion functions of the operator double() form are defined.) You can implement addition with the following member function:

Stonewt Stonewt::operator+(const Stonewt & st) const
{
    double pds = pounds + st.pounds;
    Stonewt sum(pds);
    return sum;
}

Or you can implement addition as a friend function this way:

Stonewt operator+(const Stonewt & st1, const Stonewt & st2)
{
    double pds = st1.pounds + st2.pounds;
    Stonewt sum(pds);
    return sum;
}

Either form lets you do the following:

Stonewt jennySt(9, 12);
Stonewt bennySt(12, 8);
Stonewt total;
total = jennySt + bennySt;

Also, given the Stonewt(double) constructor, each form lets you do the following:

Stonewt jennySt(9, 12);
double kennyD = 176.0;
Stonewt total;
total = jennySt + kennyD;

But only the friend function lets you do this:

Stonewt jennySt(9, 12);
double pennyD = 146.0;
Stonewt total;
total = pennyD + jennySt;

To see why, translate each addition into the corresponding function calls. First,

total = jennySt + bennySt;

becomes

total = jennySt.operator+(bennySt);   // member function

or else

total = operator+(jennySt, bennySt);  // friend function

In either case, the actual argument types match the formal arguments. Also, the member function is invoked, as required, by a Stonewt object.

Next,

total = jennySt + kennyD;

becomes

total = jennySt.operator+(kennyD);   // member function

or else

total = operator+(jennySt, kennyD);  // friend function

Again, the member function is invoked, as required, by a Stonewt object. This time, in each case, one argument is type double, which invokes the Stonewt(double) constructor to convert the argument to a Stonewt object.

By the way, having an operator double() member function defined would create confusion at this point, for that would create another option for interpretation. Instead of converting kennyD to double and performing Stonewt addition, the compiler could convert jennySt to double and perform double addition. Too many conversion functions create ambiguities.

Finally,

total = pennyD + jennySt;

becomes

total = operator+(pennyD, jennySt);  // friend function

Here, both arguments are type double, which invokes the Stonewt(double) constructor to convert them to Stonewt objects. The member function cannot be invoked, however.

total = pennyD.operator+(jennySt);   // not meaningful

The reason is that only a class object can invoke a member function. C++ will not attempt to convert pennyD to a Stonewt object. Conversion takes place for member function arguments, not for member function invokers.

The lesson here is that defining addition as a friend makes it easier for a program to accommodate automatic type conversions. The reason is that both operands become function arguments, so function prototyping comes into play for both operands.

A Choice

Given that you want to add double quantities to Stonewt quantities, you have a couple of choices. The first, which we just outlined, is to define operator+(const Stonewt &, const Stonewt &) as a friend function and have the Stonewt(double) constructor handle conversions of type double arguments to type Stonewt arguments.

The second choice is to further overload the addition operator with functions that explicitly use one type double argument:

Stonewt operator+(double x);  // member function
friend Stonewt operator+(double x, Stonewt & s);

That way, the statement

total = jennySt + kennyD; // Stonewt + double

exactly matches the operator+(double x) member function, and the statement

total = pennyD + jennySt; // double + Stonewt

exactly matches the operator+(double x, Stonewt &s) friend function. Earlier, we did something similar for Vector multiplication.

Each choice has its advantages. The first choice (relying upon implicit conversions) results in a shorter program because you define fewer functions. That also implies less work for you and fewer chances to mess up. The disadvantage is the added overhead in time and memory needed to invoke the conversion constructor whenever a conversion is needed. The second choice (additional functions explicitly matching the types), however, is the mirror image. It makes for a longer program and more work on your part, but it runs a bit faster.

If your program makes intensive use of adding double values to Stonewt objects, it may pay to overload addition to handle such cases directly. If the program just uses such addition occasionally, it's simpler to rely on automatic conversions, or, if you want to be more careful, upon explicit conversions.

Real World Note: Calling Bootstrap Functions Before main()

graphics/common.gif

The first function called in any executable is always its main() entry point. While this is true, there are a few tricks you can perform to alter this behavior. For example, consider a scheduling program that coordinates the production of golf clubs. Normally, when the program is started, information from a variety of sources is required to accurately schedule the daily production run of golf clubs. So you might want some "bootstrap" functions called first to prepare the ground for main().

A global object (i.e., an object with file scope) is precisely what you're looking for because global objects are guaranteed to be constructed before a program's main() function is called. What you can do is create a class with a default constructor that invokes all of your bootstrap functions. These could, for example, initialize various data components of the object. Then you can create a global object. The following code illustrates this technique.

class CompileRequirements
{
  private:
    // essential information
  public:
    CompileRequirements()   // default constructor
    {
      GetDataFromSales();           // various
      GetDataFromManufacturing();   // bootstrap
      GetDataFromFinance();         // functions
    }
};

//Instance of Req class has global scope
CompileRequirements Req;     // uses default constructor

int main(void)
{
  // Read Req and build schedule
  BuildScheduleFromReq();
  //
  // rest of program code
  //
}

Summary

This chapter covers many important aspects of defining and using classes. Some of the material in this chapter may seem vague to you until your own experiences enrich your understanding. Meanwhile, let's summarize the chapter.

Normally, the only way you can access private class members is by using a class method. C++ alleviates that restriction with friend functions. To make a function a friend function, declare the function in the class declaration and preface the declaration with the keyword friend.

C++ extends overloading to operators by letting you define special operator functions that describe how particular operators relate to a particular class. An operator function can be a class member function or a friend function. (A few operators can only be class member functions.) C++ lets you invoke an operator function either by calling the function or by using the overloaded operator with its usual syntax. An operator function for the operator op has this form:

operatorop(argument-list)

The argument-list represents operands for the operator. If the operator function is a class member function, then the first operand is the invoking object and isn't part of the argument-list. For example, we overloaded addition by defining an operator+() member function for the Vector class. If up, right, and result are three vectors, you can use either of the following statements to invoke vector addition:

result = up.operator+(right);
result = up + right;

For the second version, the fact that the operands up and right are type Vector tells C++ to use the Vector definition of addition.

When an operator function is a member function, the first operand is the object invoking the function. In the preceding statements, for example, the up object is the invoking object. If you want to define an operator function so that the first operand is not a class object, you must use a friend function. Then you can pass the operands to the function definition in whichever order you want.

One of the most common tasks for operator overloading is defining the << operator so that it can be used in conjunction with the cout object to display an object's contents. To allow an ostream object to be the first operand, define the operator function as a friend. To allow the redefined operator to be concatenated with itself, make the return type ostream &. Here's a general form satisfying those requirements:

ostream & operator<<(ostream & os, const c_name & obj)
{
    os << ... ;  // display object contents
    return os;
}

If, however, the class has methods that return values for the data members you want to display, you can use those methods instead of direct access in operator<<(). In that case, the function needn't (and shouldn't) be a friend.

C++ lets you establish conversions to and from class types. First, any class constructor taking a single argument acts as a conversion function, converting values of the argument type to the class type. C++ invokes the constructor automatically if you assign a value of the argument type to an object. For example, suppose you have a String class with a constructor that takes char * value as its sole argument. Then, if bean is a String object, you can use the following statement:

bean = "pinto";   // converts type char * to type String

If, however, you precede the constructor declaration with the keyword explicit, then the constructor can be used only for explicit conversions:

bean = String("pinto");   // converts type char * to type String explicitly

To convert from a class to another type, you must define a conversion function providing instruction about how to make the conversion. A conversion function must be a member function. If it is to convert to type typeName, it should have the following prototype:

operator typeName();

Note that it must have no declared return type, must have no arguments, and must (despite having no declared return type) return the converted value. For example, a function to convert type Vector to type double would have this function form:

Vector::operator double()
{
    ...
    return a_double_value;
}

Experience has shown that often it is better not to rely upon such implicit conversion functions.

As you might have noticed, classes require much more care and attention to detail than do simple C-style structures. In return, they do much more for you.

Review Questions

1:

Use a member function to overload the multiplication operator for the Stonewt class; have the operator multiply the data members by a type double value. Note that this will require carryover for the stone-pound representation. That is, twice 10 stone 8 pounds is 21 stone 2 pounds.

2:

What are the differences between a friend function and a member function?

3:

Does a nonmember function have to be a friend to access a class's members?

4:

Use a friend function to overload the multiplication operator for the Stonewt class; have the operator multiply the double value by the Stone value.

5:

Which operators cannot be overloaded?

6:

What restriction applies to overloading the following operators? = () [] ->

7:

Define a conversion function for the Vector class that converts a Vector to a type double value representing the vector's magnitude.

Programming Exercises

1:

Modify Listing 11.15 so that instead of reporting the results of a single trial for a particular target-step combination, it reports the highest, lowest, and average number of steps for N trials, where N is an integer entered by the user.

2:

Rewrite the final Time class example (Listings 11.10,11.11 and 11.12) so that all the overloaded operators are implemented using friend functions.

3:

Rewrite the Stonewt class (Listings 11.16 and 11.17) so that it has a state member governing whether the object is interpreted in stone form, integer pounds form, or floating-point pounds form. Overload the << operator to replace the show_stn() and show_lbs() methods. Overload the addition, subtraction, and multiplication operators so that one can add, subtract, and multiply Stonewt values. Test your class with a short program that uses all the class methods and friends.

4:

Rewrite the Stonewt class (Listings 11.16 and 11.17) so that it overloads all six relational operators. The operators should compare the pounds members and return a type bool value. Write a program that declares an array of six Stonewt objects and initializes the first three objects in the array declaration. Then it should use a loop to read in values used to set the remaining three array elements. Then it should report the smallest element, the largest element, and how many elements are greater or equal to 11 stone. (The simplest approach is to create a Stonewt object initialized to 11 stone and to compare the other objects with that object.)

5:

A complex number has two parts: a real part and an imaginary part. One way to write an imaginary number is this: (3.0, 4.0). Here 3.0 is the real part and 4.0 is the imaginary part. Suppose a = (A,Bi) and c = (C,Di). Here are some complex operations:

  • Addition: a + c = (A + C, (B + D)i)

  • Subtraction: a - c = (A - C, (B - D)i)

  • Multiplication: a * c = (A * C - B*D, (A*D + B*C)i)

  • Multiplication: (x a real number): x * c = (x*C,x*Di)

  • Conjugation: ~a = (A, - Bi)

Define a complex class so that the following program can use it with correct results. Note that you have to overload the << and >> operators. Many systems already have complex support in a complex.h header file, so use complex0.h to avoid conflicts. Use const whenever warranted.

#include <iostream>
using namespace std;
#include "complex0.h"  // to avoid confusion with complex.h
int main()
{
    complex a(3.0, 4.0);   // initialize to (3,4i)
    complex c;
    cout << "Enter a complex number (q to quit):\n";
    while (cin >> c)
    {
        cout << "c is " << c << '\n';
        cout << "complex conjugate is " << ~c << '\n';
        cout << "a is " << a << '\n";
        cout << "a + c is " << a + c << '\n';
        cout << "a - c is " << a - c << '\n';
        cout << "a * c is " << a * c << '\n';
        cout << "2 * c is " << 2 * c << '\n';
        cout << "Enter a complex number (q to quit):\n";
    }
    cout << "Done!\n";
    return 0;
}

Here is a sample run. Note that cin >> c, through overloading, now prompts for real and imaginary parts:

Enter a complex number (q to quit):
real: 10
imaginary: 12
c is (10,12i)
complex conjugate is (10,-12i)
a is (3,4i)
a + c is (13,16i)
a - c is (-7,-8i)
a * c is (-18,76i)
2 * c is (20,24i)
Enter a complex number (q to quit):
real: q
Done!
CONTENTS