<<
OperatorOne very useful feature 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 do this 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 a dangerous idea to alter the iostream
file and mess around with a standard interface. Instead, use the Time
class declaration to teach the Time
class how to use cout
.
<<
To teach the Time
class to use cout
, you can use a friend function. Why? Because a statement like the following uses two objects, with the ostream
class object (cout
) first:
cout << trip;
If you use a Time
member function to overload <<
, the Time
object would come first, as it did when you 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
This 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
Note that the new operator<<()
definition uses the ostream
reference os
as its first argument. Normally, os
refers 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.
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.
<<
The implementation just presented has a problem. Statements such as this work fine:
cout << trip;
But the implementation doesn’t allow you to combine the redefined <<
operator with the ones cout
normally uses:
cout << "Trip time: " << trip << " (Tuesday) "; // 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 a reference to an ostream
object. In particular, it returns a reference to the invoking object—cout
, in this case. Thus, the expression (cout << x)
is itself the ostream
object cout
, and it can be used to the left of the <<
operator.
You can take the same approach with the friend function. You 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 &
. Recall that this means 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) "; // can do
Let’s break this into separate steps to see how it works. First, the following invokes the particular ostream
definition of <<
that displays a string and returns the cout
object:
cout << "Trip time: "
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) ";
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) ";
The program now finishes up by using the ostream
definition of <<
for strings to display the final string.
As a point of interest, this version of operator<<()
also can be used for file output:
#include <fstream>
...
ofstream fout;
fout.open("savetime.txt");
Time trip(12, 40);
fout << trip;
The last statement becomes this:
operator<<(fout, trip);
And as Chapter 8 points out, the properties of class inheritance allow an ostream
reference to refer to ostream
objects and to ofstream
objects.
In general, to overload the <<
operator to display an object of class c_name
, you use a friend function with a definition in 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 of these as an inline function because the code is so short. (When the definition is also the prototype, as in this case, you use the friend
prefix.)
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 is also the prototype.
// mytime3.h -- Time class with friends
#ifndef MYTIME3_H_
#define MYTIME3_H_
#include <iostream>
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 std::ostream & operator<<(std::ostream & os, const Time & t);
};
#endif
Listing 11.11 shows the revised set of definitions. Note again that the methods use the Time::
qualifier, whereas the friend function does not. Also note that because mytime3.h
includes iostream
and provides the using
declaration std::ostream
, including mytime3.h
in mytime3.cpp
provides support for using ostream
in the implementation file.
// mytime3.cpp -- implementing 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;
}
std::ostream & operator<<(std::ostream & os, const Time & t)
{
os << t.hours << " hours, " << t.minutes << " minutes";
return os;
}
Listing 11.12 shows a sample program. Technically, usetime3.cpp
doesn’t have to include iostream
because mytime3.h
already includes that file. However, as a user of the Time
class, you don’t necessarily know which files are included in the class code, so you would take the responsibility of declaring those header files that you know your part of the code needs.
//usetime3.cpp -- using the fourth draft of the Time class
// compile usetime3.cpp and mytime3.cpp together
#include <iostream>
#include "mytime3.h"
int main()
{
using std::cout;
using std::endl;
Time aida(3, 35);
Time tosca(2, 48);
Time temp;
cout << "Aida and Tosca:
";
cout << aida<<"; " << tosca << endl;
temp = aida + tosca; // operator+()
cout << "Aida + Tosca: " << temp << endl;
temp = aida* 1.17; // member operator*()
cout << "Aida * 1.17: " << temp << endl;
cout << "10.0 * Tosca: " << 10.0 * tosca << endl;
return 0;
}
Here is the output of the program in Listings 11.10, 11.11, and 11.12:
Aida and Tosca:
3 hours, 35 minutes; 2 hours, 48 minutes
Aida + Tosca: 6 hours, 23 minutes
Aida * 1.17: 4 hours, 11 minutes
10.0 * Tosca: 28 hours, 0 minutes
For many operators, you have a choice between using member functions or nonmember functions to implement operator overloading. Typically, the nonmember version is a friend function so that it can directly access the private data for a class. For example, consider the addition operator for the Time
class. It has this prototype in the Time
class declaration:
Time operator+(const Time & t) const; // member version
Instead, the class could use the following prototype:
// nonmember version
friend Time operator+(const Time & t1, const Time & t2);
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.
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 fewer parameter because one operand is passed implicitly as the invoking object.
Either of these two prototypes matches the expression T2 + T3
, where T2
and T3
are type Time
objects. That is, the compiler can convert the statement
T1 = T2 + T3;
to either of the following:
T1 = T2.operator+(T3); // member function
T1 = operator+(T2, T3); // nonmember 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 an ambiguity error, leading to a compilation 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. Sometimes, depending on the class design, the nonmember version may have an advantage, particularly if you have defined type conversions for the class. The section “Conversions and Friends,” near the end of this chapter, discusses this situation further.
Let’s look at another class design that uses operator overloading and friends—a class representing vectors. This class also illustrates 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 shown here in other contexts. A vector, as the term is used in engineering and physics, is a quantity that has 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 another 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 a computer science version, the vector
template class.) The following sidebar tells you more about vectors, but understanding them completely isn’t necessary for following the C++ aspects of the examples.
Vectors are 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, in this section 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:
• You can describe a vector by its magnitude (length) and direction (an angle).
• You can represent a vector by its x and y components.
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 covered with the Chapter 7 program that converts between rectangular and polar coordinates.
Sometimes one form is more convenient, sometimes the other, so you’ll incorporate both representations into the class description. (See the sidebar “Multiple Representations and Classes,” later in this chapter.) Also you’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. Also the program uses enum
to create a couple constants (RECT
and POL
) for identifying the two representations. (We covered that technique in Chapter 10, so we may as well use it!)
// vect.h -- Vector class with <<, mode state
#ifndef VECTOR_H_
#define VECTOR_H_
#include <iostream>
namespace VECTOR
{
class Vector
{
public:
enum Mode {RECT, POL};
// RECT for rectangular, POL for Polar modes
private:
double x; // horizontal value
double y; // vertical value
double mag; // length of vector
double ang; // direction of vector in degrees
Mode mode; // RECT or POL
// private methods for setting values
void set_mag();
void set_ang();
void set_x();
void set_y();
public:
Vector();
Vector(double n1, double n2, Mode form = RECT);
void reset(double n1, double n2, Mode form = RECT);
~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 POL
void rect_mode(); // set mode to RECT
// 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 std::ostream &
operator<<(std::ostream & os, const Vector & v);
};
} // end namespace VECTOR
#endif
Notice that the four functions in Listing 11.13 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 reset()
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 Chapter 4, “Compound Types,” and Chapter 7, C++’s built-in math functions use angles in radians, so the functions build conversion to and from degrees into the methods. The Vector
class 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.
// vect.cpp -- methods for the Vector class
#include <cmath>
#include "vect.h" // includes <iostream>
using std::sqrt;
using std::sin;
using std::cos;
using std::atan;
using std::atan2;
using std::cout;
namespace VECTOR
{
// compute degrees in one radian
const double Rad_to_deg = 45.0 / atan(1.0);
// should be about 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 = RECT;
}
// 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, Mode form)
{
mode = form;
if (form == RECT)
{
x = n1;
y = n2;
set_mag();
set_ang();
}
else if (form == POL)
{
mag = n1;
ang = n2 / Rad_to_deg;
set_x();
set_y();
}
else
{
cout << "Incorrect 3rd argument to Vector() -- ";
cout << "vector set to 0
";
x = y = mag = ang = 0.0;
mode = RECT;
}
}
// reset vector from rectangular coordinates if form is
// RECT (the default) or else from polar coordinates if
// form is POL
void Vector:: reset(double n1, double n2, Mode form)
{
mode = form;
if (form == RECT)
{
x = n1;
y = n2;
set_mag();
set_ang();
}
else if (form == POL)
{
mag = n1;
ang = n2 / Rad_to_deg;
set_x();
set_y();
}
else
{
cout << "Incorrect 3rd argument to Vector() -- ";
cout << "vector set to 0
";
x = y = mag = ang = 0.0;
mode = RECT;
}
}
Vector::~Vector() // destructor
{
}
void Vector::polar_mode() // set to polar mode
{
mode = POL;
}
void Vector::rect_mode() // set to rectangular mode
{
mode = RECT;
}
// 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);
}
// multiply 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 RECT,
// else display polar coordinates if mode is POL
std::ostream & operator<<(std::ostream & os, const Vector & v)
{
if (v.mode == Vector::RECT)
os << "(x,y) = (" << v.x << ", " << v.y << ")";
else if (v.mode == Vector::POL)
{
os << "(m,a) = (" << v.mag << ", "
<< v.ang * Rad_to_deg << ")";
}
else
os << "Vector object mode is invalid";
return os;
}
} // end namespace VECTOR
You could design the class differently. For example, the object could store the rectangular coordinates and not the polar coordinates. In that case, the computation of polar coordinates could be moved to the magval()
and angval()
methods. For applications in which conversions are seldom used, this could be a more efficient design. Also the reset()
method isn’t really needed. Suppose shove
is a Vector
object and that you have the following code:
shove.reset(100,300);
You can get the same result by using a constructor instead:
shove = Vector(100,300); // create and assign a temporary object
However, the set()
method alters the contents of shove
directly, whereas using the constructor adds the extra steps of creating a temporary object and assigning it to shove
.
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 because 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 the Vector
class in more detail next.
3.145.35.194