CONTENTS

Chapter 15. FRIENDS, EXCEPTIONS, AND MORE

You will learn about the following in this chapter:

This chapter ties up some loose ends, then ventures into some of the most recent additions to the C++ language. The loose ends include friend classes, friend member functions, and nested classes, which are classes declared within other classes. The recent additions discussed here are exceptions, RTTI, and improved type cast control. C++ exception handling provides a mechanism for dealing with unusual occurrences that otherwise would bring a program to a halt. RTTI, or runtime type information, is a mechanism for identifying object types. The new type cast operators improve the safety of type casts. These last three facilities are fairly new to C++, and older compilers do not support them.

Friends

Several examples in this book have used friend functions as part of the extended interface for a class. Such functions are not the only kinds of friends a class can have. A class also can be a friend. In that case, any method of the friend class can access private and protected members of the original class. Also, you can be more restrictive and designate just particular member functions of a class to be friends to another class. A class defines which functions, member functions, or classes are friends; friendship cannot be imposed from the outside. Thus, although friends do grant outside access to a class's private portion, they don't really violate the spirit of object-oriented programming. Instead, they provide more flexibility to the public interface.

Friend Classes

When might you want to make one class a friend to another? Let's look at an example. Suppose you must program a simple simulation of a television and a remote control. You decide to define a Tv class representing a television and a Remote class representing a remote control. Clearly, there should be some sort of relationship between these classes, but what kind? A remote control is not a television and vice versa, so the is-a relationship of public inheritance doesn't apply. Nor is either a component of the other, so the has-a relationship of containment or of private or protected inheritance doesn't apply. What is true is that a remote control can modify the state of a television, and this suggests making the Remote class a friend to the Tv class.

First, let's define the Tv class. You can represent a television with a set of state members, that is, variables that describe various aspects of the television. Here are some of the possible states:

The tuning mode reflects the fact that, in the U.S., the spacing between channels for channels 14 and up is different for cable reception than it is for UHF broadcast reception. The input selection chooses between TV, which could be either cable or broadcast TV, and a VCR. Some sets may offer more choices, but this list is enough for our purposes.

Also, a television has some parameters that aren't state variables. For example, televisions vary in the number of channels they can receive, and you can include a member to track that value.

Next, you must provide the class with methods for altering these settings. Many television sets these days hide their controls behind panels, but it's still possible with most televisions to change channels, and so on, without a remote control. However, often you can go up or down one channel at a time but can't select a channel at random. Similarly, there's usually a button for increasing the volume and one for decreasing the volume.

A remote control should duplicate the controls built in to the television. Many of its methods can be implemented by using Tv methods. In addition, a remote control typically provides random access channel selection. That is, you can go directly from channel 2 to channel 20 without going through all the intervening channels. Also, many remotes can work in two modes梐s a television controller and as a VCR controller.

These considerations suggest a definition like that shown in Listing 15.1. The definition includes several constants defined as enumerations. The statement making Remote a friend class is this:

friend class Remote;

A friend declaration can appear in a public, private, or protected section; the location makes no difference. Because the Remote class mentions the Tv class, the compiler has to know about the Tv class before it can handle the Remote class. The simplest way to accomplish this is to define the Tv class first. Alternatively, you can use a forward declaration; we'll discuss that option soon.

Compatibility Note

graphics/hands.gif

If your compiler doesn't support the bool type, use int, 0, and 1 instead of bool, false, and true.

Listing 15.1 tv.h
// tv.h -- Tv and Remote classes
#ifndef TV_H_
#define TV_H_

class Tv
{
public:
    friend class Remote;   // Remote can access Tv private parts
    enum {Off, On} ;
    enum {MinVal,MaxVal = 20};
    enum {Antenna, Cable};
    enum {TV, VCR};

    Tv(int s = Off, int mc = 100) : state(s), volume(5),
        maxchannel(mc), channel(2), mode(Cable), input(TV) {}
    void onoff() {state = (state == On)? Off : On;}
    bool ison() const {return state == On;}
    bool volup();
    bool voldown();
    void chanup();
    void chandown();
    void set_mode() {mode = (mode == Antenna)? Cable : Antenna;}
    void set_input() {input = (input == TV)? VCR : TV;}
    void settings() const; // display all settings
private:
    int state;             // on or off
    int volume;            // assumed to be digitized
    int maxchannel;        // maximum number of channels
    int channel;           // current channel setting
    int mode;              // broadcast or cable
    int input;             // TV or VCR
};

class Remote
{
private:
    int mode;              // controls TV or VCR
public:
    Remote(int m = Tv::TV) : mode(m) {}
    bool volup(Tv & t) { return t.volup();}
    bool voldown(Tv & t) { return t.voldown();}
    void onoff(Tv & t) { t.onoff(); }
    void chanup(Tv & t) {t.chanup();}
    void chandown(Tv & t) {t.chandown();}
    void set_chan(Tv & t, int c) {t.channel = c;}
    void set_mode(Tv & t) {t.set_mode();}
    void set_input(Tv & t) {t.set_input();}
};
#endif

Most of the class methods are defined inline. Note that each Remote method other than the constructor takes a reference to a Tv object as an argument. That reflects that a remote has to be aimed at a particular TV. Listing 15.2 shows the remaining definitions. The volume-setting functions change the volume member by one unit unless the sound has reached its minimum or maximum setting. The channel selection functions use wraparound, with the lowest channel setting, taken to be 1, immediately following the highest channel setting, maxchannel.

Many of the methods use the conditional operator to toggle a state between two settings:

void onoff() {state = (state == On)? Off : On;}

Provided that the two state values are 0 and 1, this can be done more compactly using the combined bitwise exclusive OR and assignment operator (^=) discussed in Appendix E, "Other Operators:"

void onoff() {state ^= 1;}

In fact, you could store up to eight bivalent state settings in a single unsigned char variable and toggle them individually, but that's another story, one made possible by the bitwise operators discussed in Appendix E.

Listing 15.2 tv.cpp
// tv.cpp -- methods for the Tv class (Remote methods are inline)
#include <iostream>
using namespace std;
#include "tv.h"

bool Tv::volup()
{
    if (volume < MaxVal)
    {
        volume++;
        return true;
    }
    else
        return false;
}
bool Tv::voldown()
{
    if (volume > MinVal)
    {
        volume--;
        return true;
    }
    else
        return false;
}

void Tv::chanup()
{
    if (channel < maxchannel)
        channel++;
    else
        channel = 1;
}

void Tv::chandown()
{
    if (channel > 1)
        channel--;
    else
        channel = maxchannel;
}

void Tv::settings() const
{
    cout << "TV is " << (state == Off? "Off" : "On") << '\n';
    if (state == On)
    {
        cout << "Volume setting = " << volume << '\n';
        cout << "Channel setting = " << channel << '\n';
        cout << "Mode = "
            << (mode == Antenna? "antenna" : "cable") << '\n';
        cout << "Input = "
            << (input == TV? "TV" : "VCR") << '\n';
    }
}

Next, Listing 15.3 is a short program that tests some of the features. The same controller is used to control two separate televisions.

Listing 15.3 use_tv.cpp
//use_tv.cpp
#include <iostream>
using namespace std;
#include "tv.h"

int main()
{
    Tv s20;
    cout << "Initial settings for 20\" TV:\n";
    s20.settings();
    s20.onoff();
    s20.chanup();
    cout << "\nAdjusted settings for 20\" TV:\n";
    s20.settings();

    Remote grey;

    grey.set_chan(s20, 10);
    grey.volup(s20);
    grey.volup(s20);
    cout << "\n20\" settings after using remote:\n";
    s20.settings();

    Tv s27(Tv::On);
    s27.set_mode();
    grey.set_chan(s27,28);
    cout << "\n27\" settings:\n";
    s27.settings();

    return 0;
}

Here is the program output:

Initial settings for 27" TV:
TV is Off

Adjusted settings for 27" TV:
TV is On
Volume setting = 5
Channel setting = 3
Mode = cable
Input = TV

27" settings after using remote:
TV is On
Volume setting = 7
Channel setting = 10
Mode = cable
Input = TV

34" settings:
TV is On
Volume setting = 5
Channel setting = 2
Mode = antenna
Input = TV

The main point to this exercise is that class friendship is a natural idiom in which to express some relationships. Without some form of friendship you would either have to make the private parts of the Tv class public or else construct some awkward, larger class that encompasses both a television and a remote control. And that solution wouldn't reflect the fact that a single remote control can be used with several televisions.

Friend Member Functions

Looking at the code for the last example, you may notice that most of the Remote methods are implemented using the public interface for the Tv class. This means that those methods don't really need friend status. Indeed, the only Remote method that accesses a private Tv member directly is Remote::set_chan(), so that's the only method that needs to be a friend. You do have the option of making just selected class members friends to another class rather than making the entire class a friend, but it's a bit more awkward. You need to be careful about the order in which you arrange the various declarations and definitions. Let's see why.

The way to make Remote::set_chan() a friend to the Tv class is to declare it as a friend in the Tv class declaration:

class Tv
{
    friend void Remote::set_chan(Tv & t, int c);
    ...
};

However, for the compiler to process this statement, it needs to have already seen the Remote definition. Otherwise, it won't know that Remote is a class and that set_chan() is a method of that class. That suggests putting the Remote definition above the Tv definition. But the fact that Remote methods mention Tv objects means that the Tv definition should come above the Remote definition. Part of the way around the circular dependence is to use a forward declaration. That means inserting the statement

class Tv;              // forward declaration

above the Remote definition. This provides the following arrangement:

class Tv;  // forward declaration
class Remote { ... };
class Tv { ... };

Could you use the following arrangement instead?

class Remote;          // forward declaration
class Tv { ... };
class Remote { ... };

The answer is no. The reason, as mentioned earlier, is that when the compiler sees that a Remote method is declared as a friend in the Tv class declaration, the compiler needs to have already viewed the declaration of the Remote class in general and of the set_chan() method in particular.

Another difficulty remains. In Listing 15.1, the Remote declaration contained inline code such as the following:

void onoff(Tv & t) { t.onoff(); }

Because this calls a Tv method, the compiler needs to have seen the Tv class declaration at this point so that it knows what methods Tv has. But, as you've seen, that declaration necessarily follows the Remote declaration. The solution to this problem is to restrict Remote to method declarations and to place the actual definitions after the Tv class. This leads to the following ordering:

class Tv;               // forward declaration
class Remote { ... };     // Tv-using methods as prototypes only
class Tv { ... };
// put Remote method definitions here

The Remote prototypes look like this:

void onoff(Tv & t);

All the compiler needs to know when inspecting this prototype is that Tv is a class, and the forward declaration supplies that information. By the time the compiler reaches the actual method definitions, it has already read the Tv class declaration and has the added information needed to compile those methods. By using the inline keyword in the method definitions, you still can make the methods inline methods. Listing 15.4 shows the revised header file.

Listing 15.4 tvfm.h
// tvfm.h -- Tv and Remote classes using a friend member
#ifndef TVFM_H_
#define TVFM_H_

class Tv;                       // forward declaration

class Remote
{
public:
    enum State{ Off, On};
    enum {MinVal,MaxVal = 20};
    enum {Antenna, Cable};
    enum {TV, VCR};
private:
    int mode;
public:
    Remote(int m = TV) : mode(m) {}
    bool volup(Tv & t);         // prototype only
    bool voldown(Tv & t);
    void onoff(Tv & t) ;
    void chanup(Tv & t) ;
    void chandown(Tv & t) ;
    void set_mode(Tv & t) ;
    void set_input(Tv & t);
    void set_chan(Tv & t, int c);

};

class Tv
{
public:
    friend void Remote::set_chan(Tv & t, int c);
    enum State{ Off, On};
    enum {MinVal,MaxVal = 20};
    enum {Antenna, Cable};
    enum {TV, VCR};

    Tv(int s = Off, int mc = 100) : state(s), volume(5),
        maxchannel(mc), channel(2), mode(Cable), input(TV) {}
    void onoff() {state = (state == On)? Off : On;}
    bool ison() const { return state == On;}
    bool volup();
    bool voldown();
    void chanup();
    void chandown();
    void set_mode() {mode = (mode == Antenna)? Cable : Antenna;}
    void set_input() {input = (input == TV)? VCR : TV;}
    void settings() const;
private:
    int state;
    int volume;
    int maxchannel;
    int channel;
    int mode;
    int input;
};

// Remote methods as inline functions
inline bool Remote::volup(Tv & t) { return t.volup();}
inline bool Remote::voldown(Tv & t) { return t.voldown();}
inline void Remote::onoff(Tv & t) { t.onoff(); }
inline void Remote::chanup(Tv & t) {t.chanup();}
inline void Remote::chandown(Tv & t) {t.chandown();}
inline void Remote::set_mode(Tv & t) {t.set_mode();}
inline void Remote::set_input(Tv & t) {t.set_input();}
inline void Remote::set_chan(Tv & t, int c) {t.channel = c;}
#endif

This version behaves the same as the original. The difference is that just one Remote method is a friend to the Tv class instead of all the Remote methods. Figure 15.1 illustrates this difference.

Figure 15.1. Class friends versus class member friends.

graphics/15fig01.gif

By the way, making the entire Remote class a friend doesn't need a forward declaration because the friend statement itself identifies Remote as a class:

friend class Remote;

Other Friendly Relationships

Other combinations of friends and classes are possible. Let's take a brief look at some of them now. Suppose the advance of technology brings interactive remote controllers. For example, an interactive remote control unit might let you enter a response to some question posed on a television program, and the television might activate a buzzer in your controller if your response were wrong. Ignoring the possibility of television using such facilities to program the viewers, let's just look at the C++ programming aspects. The new setup would benefit from mutual friendship, with some Remote methods being able to affect a Tv object, as before, and with some Tv methods being able to affect a Remote object. This can be accomplished by making the classes friends to each other. That is, Tv will be a friend to Remote in addition to Remote being a friend to Tv. One point to keep in mind is that a Tv method that uses a Remote object can be prototyped before the Remote class declaration but must be defined after the declaration so that the compiler will have enough information to compile the method. The setup would look like this:

class Tv
{
friend class Remote;
public:
    void buzz(Remote & r);
    ...
};
class Remote
{
friend class Tv;
public:
    void Bool volup(Tv & t) {  t.volup(); }
    ...
};
inline void Tv::buzz(Remote & r)
{
   ...
}

Because the Remote declaration follows the Tv declaration, Remote::volup() can be defined in the class declaration. The Tv::buzz() method, however, has to be defined outside the Tv declaration so that the definition can follow the Remote declaration. If you don't want buzz() to be inline, define it in a separate method definitions file.

Shared Friends

Another use for friends is when a function needs to access private data in two separate classes. Logically, such a function should be a member function of each class, but that's impossible. It could be a member of one class and a friend to the other, but sometimes it's more reasonable to make the function friends to both. Suppose, for example, that you have a Probe class representing some sort of programmable measuring device and an Analyzer class representing some sort of programmable analyzing device. Each has an internal clock, and you would like to be able to synchronize the two clocks. You could do something along the following lines:

class Analyzer;  // forward declaration
class Probe
{
    friend void sync(Analyzer & a, const Probe & p);  // sync a to p
    friend void sync(Probe & p, const Analyzer & a);  // sync p to a
    ...
};
class Analyzer
{
    friend void sync(Analyzer & a, const Probe & p);  // sync a to p
    friend void sync(Probe & p, const Analyzer & a);  // sync p to a
    ...
};
// define the friend functions
inline void sync(Analyzer & a, const Probe & p)
{
    ...
}
inline void sync(Probe & p, const Analyzer & a)
{
    ...
}

The forward declaration enables the compiler to know that Analyzer is a type when it reaches the friend declarations in the Probe class declaration.

Nested Classes

In C++, you can place a class declaration inside another class. The class declared within another is called a nested class, and it helps avoid name clutter by giving the new type class scope. Member functions of the class containing the declaration can create and use objects of the nested class. The outside world can use the nested class only if the declaration is in the public section and if you use the scope resolution operator. (Older versions of C++, however, don't allow nested classes or else implement the concept incompletely.)

Nesting classes is not the same as containment. Containment, recall, means having a class object as a member of another class. Nesting a class, on the other hand, does not create a class member. Instead, it defines a type that is known just locally to the class containing the nested class declaration.

The usual reasons for nesting a class are to assist the implementation of another class and to avoid name conflicts. The Queue class example (Chapter 12, "Classes and Dynamic Memory Allocation," Listing 12.8) provided a disguised case of nested classes by nesting a structure definition:

class Queue
{
// class scope definitions
    // Node is a nested structure definition local to this class
    struct Node { Item item; struct Node * next;};

    ...
};

Because a structure is a class whose members are public by default, Node really is a nested class. However, this definition doesn't take advantage of class abilities. In particular, it lacks an explicit constructor. Let's remedy that now.

First, let's find where Node objects are created in the Queue example. Examining the class declaration (Listing 11.12) and the methods definitions (Listing 12.9) reveals that the only place in which Node objects are created is in the enqueue()method:

bool Queue::enqueue(const Item & item)
{
    if (isfull())
       return false;
    Node * add = new Node;  // create node
    if (add == NULL)
       return false;      // quit if none available
    add->item = item;       // set node pointers
    add->next = NULL;
    ...
}

Here the code explicitly assigns values to the Node members after creating a Node. This is the sort of work that more properly is done by a constructor.

Knowing now where and how a constructor should be used, you can provide an appropriate constructor definition:

class Queue
{
// class scope definitions
    // Node is a nested class definition local to this class
    class Node
    {
    public:
        Item item;
        Node * next;
        Node(const Item & i) : item(i), next(0) {  }
    };
    ...
};

This constructor initializes the node's item member to i and sets the next pointer to 0, which is one way of writing the null pointer in C++. (Using NULL would require including a header file that defines NULL.) Because all nodes created by the Queue class have next initially set to the null pointer, this is the only constructor the class needs.

Next, rewrite enqueue() using the constructor:

bool Queue::enqueue(const Item & item)
{
    if (isfull())
        return false;
    Node * add = new Node(item);  // create, initialize node
    if (add == 0)
        return fFalse;           // quit if none available
    ...
}

This makes the code for enqueue() a bit shorter and a bit safer, for it automates initialization rather than requiring that the programmer remember correctly what should be done.

This example defined the constructor in the class declaration. Suppose you wanted to define it in a methods file, instead. The definition must reflect that the Node class is defined within the Queue class. This is accomplished by using the scope resolution operator twice:

Queue::Node::Node(const Item & i) : item(i), next(0) { }

Nested Classes and Access

Two kinds of access pertain to nested classes. First, where a nested class is declared controls the scope of the nested class; that is, it establishes which parts of a program can create objects of that class. Second, as with any class, the public, protected, and private sections of a nested class provide access control to class members. Where and how a nested class can be used depends upon both scope and access control. Let's examine these points further.

Scope

If the nested class is declared in a private section of a second class, it is known only to that second class. This applies, for example, to the Node class nested in the Queue declaration in the last example. (It may appear that Node was defined before the private section, but remember that private is the default access for classes.) Hence, Queue members can use Node objects and pointers to Node objects, but other parts of a program won't even know that the Node class exists. If you were to derive a class from Queue, Node would be invisible to that class, too, because a derived class can't directly access the private parts of a base class.

If the nested class is declared in a protected section of a second class, it is visible to that class but invisible to the outside world. However, in this case, a derived class would know about the nested class and could directly create objects of that type.

If a nested class is declared in a public section of a second class, it is available to the second class, to classes derived from the second class, and, because it's public, to the outside world. However, because the nested class has class scope, it has to be used with a class qualifier in the outside world. For example, suppose you have this declaration:

class Team
{
public:
      class Coach { ... };
      ...
};

Now suppose you have an unemployed coach, one who belongs to no team. To create a Coach object outside of the Team class, you can do this:

Team::Coach forhire;  // create a Coach object outside the Team class

These same scope considerations apply to nested structures and enumerations, too. Indeed, many programmers use public enumerations to provide class constants that can be used by client programmers. For example, the many implementations of classes defined to support the iostream facility use this technique to provide various formatting options, as we've touched upon earlier and will explore more fully in Chapter 17, "Input, Output, and Files." Table 15.1 summarizes scope properties for nested classes, structures, and enumerations.

Table 15.1. Scope Properties for Nested Classes, Structures, and Enumerations
Where Declared in Nesting Class Available to Nesting Class Available to Classes Derived from the Nesting Class Available to the Outside World
Private section Yes No No
Protected section Yes Yes No
Public section Yes Yes Yes, with class qualifier
Access Control

After a class is in scope, access control comes into play. The same rules govern access to a nested class that govern access to a regular class. Declaring the Node class in the Queue class declaration does not grant the Queue class any special access privileges to the Node class, nor does it grant the Node class any special access privileges to the Queue class. Thus, a Queue class object can access only the public members of a Node object explicitly. For that reason, the Queue example made all the members of the Node class public. This violates the usual practice of making data members private, but the Node class is an internal implementation feature of the Queue class and is not visible to the outside world. That's because the Node class is declared in the private section of the Queue class. Thus, although Queue methods can access Node members directly, a client using the Queue class cannot do so.

In short, the location of a class declaration determines the scope or visibility of a class. Given that a particular class is in scope, the usual access control rules (public, protected, private, friend) determine the access a program has to members of the nested class.

Nesting in a Template

You've seen that templates are a good choice for implementing container classes such as the Queue class. You may be wondering if having a nested class poses any problems to converting the Queue class definition to a template. The answer is no. Listing 15.5 shows how this conversion can be made. As is common for class templates, the header file includes the class template along with method function templates.

Listing 15.5 queuetp.h
// queuetp.h -- queue template with a nested class
#ifndef QUEUETP_H_
#define QUEUETP_H_
template <class Item>
class QueueTP
{
private:
    enum {Q_SIZE = 10};
    // Node is a nested class definition
    class Node
    {
    public:
        Item item;
        Node * next;
        Node(const Item & i):item(i), next(0){ }
    };
    Node * front;       // pointer to front of Queue
    Node * rear;        // pointer to rear of Queue
    int items;          // current number of items in Queue
    const int qsize;    // maximum number of items in Queue
    QueueTP(const QueueTP & q) : qsize(0) {}
    QueueTP & operator=(const QueueTP & q) { return *this; }
public:
    QueueTP(int qs = Q_SIZE);
    ~QueueTP();
    bool isempty() const
    {
        return items == 0;
    }
    bool isfull() const
    {
        return items == qsize;
    }
    int queuecount() const
    {
        return items;
    }
    bool enqueue(const Item &item); // add item to end
    bool dequeue(Item &item);       // remove item from front
};


// QueueTP methods
template <class Item>
QueueTP<Item>::QueueTP(int qs) : qsize(qs)
{
    front = rear = 0;
    items = 0;
}

template <class Item>
QueueTP<Item>::~QueueTP()
{
    Node * temp;
    while (front != 0)      // while queue is not yet empty
    {
        temp = front;       // save address of front item
        front = front->next;// reset pointer to next item
        delete temp;        // delete former front
    }
}

// Add item to queue
template <class Item>
bool QueueTP<Item>::enqueue(const Item & item)
{
    if (isfull())
        return false;
    Node * add = new Node(item);    // create node
    if (add == NULL)
        return false;       // quit if none available
    items++;
    if (front == 0)         // if queue is empty,
        front = add;        // place item at front
    else
        rear->next = add;   // else place at rear
    rear = add;             // have rear point to new node
    return true;
}

// Place front item into item variable and remove from queue
template <class Item>
bool QueueTP<Item>::dequeue(Item & item)
{
    if (front == 0)
        return false;
    item = front->item;     // set item to first item in queue
    items--;
    Node * temp = front;    // save location of first item
    front = front->next;    // reset front to next item
    delete temp;            // delete former first item
    if (items == 0)
        rear = 0;
    return true;
}

#endif

One interesting thing about this template is that Node is defined in terms of the generic type Item. Thus, a declaration like

QueueTp<double> dq;

leads to a Node defined to hold type double values, while

QueueTp<char> cq;

leads to a Node defined to hold type char values. These two Node classes are defined in two separate QueueTP classes, so there is no name conflict between the two. That is, one node is type QueueTP<double>::Node and the other is type QueueTP<char>::Node.

Listing 15.6 offers a short program for testing the new class. It creates a queue of String objects, so it should be compiled in conjunction with string1.cpp (Chapter 12).

Listing 15.6 nested.cpp
// nested.cpp -- use queue having a nested class
// compile along with string1.cpp
#include <iostream>
using namespace std;
#include "string1.h"
#include "queuetp.h"

int main()
{
    QueueTP<String> cs(5);
    String temp;

    while(!cs.isfull())
    {
        cout << "Please enter your name. You will be "
                "served in the order of arrival.\n"
                "name: ";
        cin >> temp;
        cs.enqueue(temp);
    }
    cout << "The queue is full. Processing begins!\n";

    while (!cs.isempty())
    {
        cs.dequeue(temp);
        cout << "Now processing " << temp << "...\n";
    }
    return 0;
}

Here is a sample run:

Please enter your name. You will be served in the order of arrival.
name: Kinsey Millhone
Please enter your name. You will be served in the order of arrival.
name: Adam Dalgliesh
Please enter your name. You will be served in the order of arrival.
name: Andrew Dalziel
Please enter your name. You will be served in the order of arrival.
name: Kay Scarpetta
Please enter your name. You will be served in the order of arrival.
name: Richard Jury
The queue is full. Processing begins!
Now processing Kinsey Millhone...
Now processing Adam Dalgliesh...
Now processing Andrew Dalziel...
Now processing Kay Scarpetta...
Now processing Richard Jury...

Exceptions

Programs sometimes encounter runtime problems that prevent the program from continuing normally. For example, a program may try to open an unavailable file, or it may request more memory than is available, or it may encounter values it cannot abide. Usually, programmers try to anticipate such calamities. C++ exceptions provide a powerful and flexible tool for dealing with these situations. Exceptions were added to C++ recently, and not all compilers have implemented them yet.

Before examining exceptions, let's look at some of the more rudimentary options available to the programmer. As a test case, take a function that calculates the harmonic mean of two numbers. The harmonic mean of two numbers is defined as the inverse of the average of the inverses. This can be reduced to the following expression:

2.0 * x * y / (x + y)

Note that if y is the negative of x, this formula results in division by zero, a rather undesirable operation. One way to handle this is to have the function call the abort() function if one argument is the negative of the other. The abort() function has its prototype in the cstdlib (or stdlib.h) header file. A typical implementation, if called, sends a message like "abnormal program termination" to the standard error stream (the same as the one used by cerr) and terminates the program. It also returns an implementation-dependent value indicating failure to the operating system or, if the program was initiated by another program, to the parent process. Whether abort() flushes file buffers (memory areas used to store material for transfers to and from files) depends upon the implementation. If you prefer, you can use exit(), which does flush file buffers, but without displaying a message. Listing 15.7 shows a short program using abort().

Listing 15.7 error1.cpp
//error1.cpp -- use the abort() function
#include <iostream>
using namespace std;
#include <cstdlib>
double hmean(double a, double b);

int main()
{
    double x, y, z;
    cout << "Enter two numbers: ";
    while (cin >> x >> y)
    {
        z = hmean(x,y);
        cout << "Harmonic mean of " << x << " and " << y
            << " is " << z << "\n";
        cout << "Enter next set of numbers <q to quit>: ";
    }
    cout << "Bye!\n";
    return 0;
}

double hmean(double a, double b)
{
    if (a == -b)
    {
        cout << "untenable arguments to hmean()\n";
        abort();
    }
    return 2.0 * a * b / (a + b);
}

Here's a sample run:

Enter two numbers: 3 6
Harmonic mean of 3 and 6 is 4
Enter next set of numbers <q to quit>: 10 -10
untenable arguments to hmean()
abnormal program termination

Note that calling the abort() function from hmean() terminates the program directly without returning first to main().

The program could avoid aborting by checking the values of x and y before calling the hmean() function. However, it's not safe to rely upon a programmer to know (or care) enough to perform such a check.

A more flexible approach than aborting is to use a function's return value to indicate a problem. For example, the get(void) member of the ostream class ordinarily returns the ASCII code for the next input character, but it returns the special value EOF if it encounters the end of a file. This approach doesn't work for hmean(). Any numeric value could be a valid return value, so there's no special value available to indicate a problem. In this kind of situation, you can use a pointer argument or reference argument to get a value back to the calling program and use the function return value to indicate success or failure. The istream family of overloaded >> operators uses a variant of this technique. By informing the calling program of the success or failure, you give the program the option of taking actions other than aborting. Listing 15.8 shows an example of this approach. It redefines hmean() as a bool function whose return value indicates success or failure. It adds a third argument for obtaining the answer.

Listing 15.8 error2.cpp
//error2.cpp __ return an error code
#include <iostream>
using namespace std;
#include <cfloat>  // (or float.h) for DBL_MAX

bool hmean(double a, double b, double * ans);

int main()
{
    double x, y, z;

    cout << "Enter two numbers: ";
    while (cin >> x >> y)
    {
        if (hmean(x,y,&z))
            cout << "Harmonic mean of " << x << " and " << y
                << " is " << z << "\n";
        else
            cout << "One value should not be the negative "
                << "of the other - try again.\n";
        cout << "Enter next set of numbers <q to quit>: ";
    }
    cout << "Bye!\n";
    return 0;
}

bool hmean(double a, double b, double * ans)
{
    if (a == -b)
    {
        *ans = DBL_MAX;
        return false;
    }
    else
    {
        *ans = 2.0 * a * b / (a + b);
        return true;
    }
}

Here's a sample run:

Enter two numbers: 3 6
Harmonic mean of 3 and 6 is 4
Enter next set of numbers <q to quit>: 10 -10
One value should not be the negative of the other - try again.
Enter next set of numbers <q to quit>: 1 19
Harmonic mean of 1 and 19 is 1.9
Enter next set of numbers <q to quit>: q
Bye!

Program Notes

Here, the program design allowed the user to continue, bypassing the effects of bad input. Of course, the design does rely upon the user to check the function return value, something that programmers don't always do. For example, to keep the sample programs short, most of the listings in this book don't check to see if new returns the null pointer or if cout was successful in handling output.

You could use either a pointer or a reference for the third arguments. Many programmers prefer using pointers for arguments of the built-in types, for it makes it obvious which argument is being used for the answer.

The Exception Mechanism

Now let's see how you can handle problems with the exception mechanism. A C++ exception is a response to an exceptional circumstance that arises while a program is running, such as an attempt to divide by zero. Exceptions provide a way to transfer control from one part of a program to another. Handling an exception has three components:

A program throws an exception when a problem shows up. For example, you can modify hmean() in Listing 15.7 to throw an exception instead of calling the abort() function. A throw statement, in essence, is a jump; that is, it tells a program to jump to statements at another location. The throw keyword indicates the throwing of an exception. It's followed by a value, such as a character string or an object, indicating the nature of the exception.

A program catches an exception with an exception handler at the place in a program where you want to handle the problem. The catch keyword indicates the catching of an exception. A handler begins with the keyword catch followed, in parentheses, by a type declaration indicating the type of exception to which it responds. That, in turn, is followed by a brace-enclosed block of code indicating the actions to take. The catch keyword, along with the exception type, serves as a label identifying the point in a program to which execution should jump when an exception is thrown. An exception handler also is called a catch block.

A try block identifies a block of code for which particular exceptions will be activated. It's followed by one or more catch blocks. The try block itself is indicated by the keyword try followed by a brace-enclosed block of code indicating the code for which exceptions will be noticed.

The easiest way to see how these three elements fit together is to look at a short example, such as that provided in Listing 15.9.

Listing 15.9 error3.cpp
//error3.cpp
#include <iostream>
using namespace std;
double hmean(double a, double b);

int main()
{
    double x, y, z;

    cout << "Enter two numbers: ";
    while (cin >> x >> y)
    {
        try {                   // start of try block
            z = hmean(x,y);
        }                       // end of try block
        catch (const char * s)  // start of exception handler
        {
            cout << s << "\n";
            cout << "Enter a new pair of numbers: ";
            continue;
        }                       // end of handler
        cout << "Harmonic mean of " << x << " and " << y
            << " is " << z << "\n";
        cout << "Enter next set of numbers <q to quit>: ";
    }
    cout << "Bye!\n";
    return 0;
}

double hmean(double a, double b)
{
    if (a == -b)
        throw "bad hmean() arguments: a = -b not allowed";
    return 2.0 * a * b / (a + b);
}

Here's a sample run:

Enter two numbers: 3 6
Harmonic mean of 3 and 6 is 4
Enter next set of numbers <q to quit>: 10 -10
bad hmean() arguments: a = -b not allowed
Enter a new pair of numbers: 1 19
Harmonic mean of 1 and 19 is 1.9
Enter next set of numbers <q to quit>: q
Bye!

Program Notes

The try block looks like this:

try {                    // start of try block
    z = hmean(x,y);
}                        // end of try block

If any statement in this block leads to an exception being thrown, the catch blocks after this block will handle the exception. If the program called hmean() somewhere else outside this (and any other) try block, it wouldn't have the opportunity to handle an exception.

Throwing an exception looks like this:

if (a == -b)
    throw "bad hmean() arguments: a = -b not allowed";

In this case, the thrown exception is the string "bad hmean() arguments: a = -b not allowed". Executing the throw is a bit like executing a return statement in that it terminates function execution. However, instead of returning control to the calling program, a throw causes a program to back up through the sequence of current function calls until it finds the function containing the try block. In Listing 15.9, that function is the same as the calling function. Soon you'll see an example involving backing up more than one function. Meanwhile, in this case, the throw passes program control back to main(). There, the program looks for an exception handler (following the try block) that matches the type of exception thrown.

The handler, or catch block, looks like this:

catch (char * s)  // start of exception handler
{
    cout << s << "\n";
    cout << "Enter a new pair of numbers: ";
    continue;
}                       // end of handler

It looks a bit like a function definition, but it's not. The keyword catch identifies this as a handler, and the char * s means that this handler matches a thrown exception that is a string. This declaration of s acts much like a function argument definition in that a matching thrown exception is assigned to s. Also, if an exception does match this handler, the program executes the code within the braces.

If a program completes executing statements in a try block without any exceptions being thrown, it skips the catch block or blocks after the try block and goes to the first statement following the handlers. So when the sample run processed the values 3 and 6, program execution went directly to the output statement reporting the result.

Let's trace through the events in the sample run after the values 10 and -10 are passed to the hmean() function. The if test causes hmean() to throw an exception. This terminates execution of hmean(). Searching back, the program determines that hmean() was called from within a try block in main(). It then looks for a catch block with a type matching the exception type. The single catch block present has a char * parameter, so it does match. Detecting the match, the program assigns the string "bad hmean() arguments: a = -b not allowed" to the variable s. Next, the program executes the code in the handler. First, it prints s, which is the caught exception. Then it prints instructions to the user to enter new data. Finally, it executes a continue statement, which causes the program to skip the rest of the while loop and jump to its beginning again. The fact that the continue takes the program to the beginning of the loop illustrates the fact that handler statements are part of the loop and that the catch line acts like a label directing program flow (see Figure 15.2).

Figure 15.2. Program flow with exceptions.

graphics/15fig02.gif

You might be wondering what happens if a function throws an exception and there's no try block or else no matching handler. By default, the program eventually calls the abort() function, but you can modify that behavior. We'll return to this topic later.

Exception Versatility

C++ exceptions offer versatility, for the try block lets you select which code gets checked for exceptions and the handlers let you specify what gets done. For example, in Listing 15.9, the try block was inside the loop, so program execution continued inside the loop after the exception was handled. By placing the loop inside the try block, you can make an exception transfer execution to outside the loop, thus terminating the loop. Listing 15.10 illustrates that. It also demonstrates two more points:

To qualify a function prototype to indicate the kinds of exceptions it throws, append an exception specification, which consists of the keyword throw followed by a comma-separated list of exception types enclosed in parentheses:

double hmean(double a, double b) throw(const char *);

This accomplishes two things. First, it tells the compiler what sort of exception or exceptions a function throws. If the function then throws some other type of exception, the program will react to the faux pas by calling (eventually) the abort() function. (We'll examine this behavior and how it can be modified in more detail later.) Second, using an exception specification alerts anyone who reads the prototype that this particular function throws an exception, reminding the reader that he or she may want to provide a try block and a handler. Functions that throw more than one kind of exception can provide a comma-separated list of exception types; the syntax imitates that of an argument list for a function prototype. For example, the following prototype indicates a function that can throw either a char * exception or a double exception:

double multi_err(double z) throw(const char *, double);

The same information that appears in a prototype, as you can see in Listing 15.10, also should appear in the function definition.

Using empty parentheses in the exception specification indicates that the function does not throw exceptions:

double simple(double z) throw(); // doesn't throw an exception

Listing 15.10, as mentioned earlier, places the entire while loop inside the try block. It also adds a second exception-throwing function, gmean(). This function returns the geometric mean of two numbers, which is defined as the square root of their product. This function isn't defined for negative arguments, which provides grounds for throwing an exception. Like hmean(), gmean() throws a string-type exception, so the same catch block will catch exceptions from either of these two functions.

Listing 15.10 error4.cpp
//error4.cpp
#include <iostream>
using namespace std;
#include <cmath> // or math.h, unix users may need -lm flag
double hmean(double a, double b) throw(const char *);
double gmean(double a, double b) throw(const char *);

int main()
{
    double x, y, z;
    cout << "Enter two numbers: ";
    try {                // start of try block
        while (cin >> x >> y)
        {
            z = hmean(x,y);
            cout << "Harmonic mean of " << x << " and " << y
                << " is " << z << "\n";
            cout << "Geometric mean of " << x << " and " << y
                << " is " << gmean(x,y) << "\n";
            cout << "Enter next set of numbers <q to quit>: ";
        }
    }                   // end of try block
    catch (const char * s)    // start of catch block
    {
        cout << s << "\n";
        cout << "Sorry, you don't get to play any more. ";
    }                   // end of catch block
    cout << "Bye!\n";
    return 0;
}

double hmean(double a, double b) throw(const char *)
{
    if (a == -b)
        throw "bad hmean() arguments: a = -b not allowed";
    return 2.0 * a * b / (a + b);
}

double gmean(double a, double b) throw(const char *)
{
    if (a < 0 || b < 0)
        throw "bad gmean() arguments: negative values not allowed";
    return sqrt(a * b);
}

Here's a sample run that gets terminated by bad input for the hmean() function:

Enter two numbers: 1 100
Harmonic mean of 1 and 100 is 1.9802
Geometric mean of 1 and 100 is 10
Enter next set of numbers <q to quit>: 10 -10
bad hmean() arguments: a = -b not allowed
Sorry, you don't get to play any more. Bye!

Because the exception handler is outside the loop, bad input terminates the loop. After the program finishes the code in the handler, it proceeds to the next line in the program, which prints Bye!.

For comparison, here's a sample run that gets terminated by bad input for the gmean() function:

Enter two numbers: 1 100
Harmonic mean of 1 and 100 is 1.9802
Geometric mean of 1 and 100 is 10
Enter next set of numbers <q to quit>: 3 -15
Harmonic mean of 3 and -15 is 7.5
bad gmean() arguments: negative values not allowed
Sorry, you don't get to play any more. Bye!

The message reveals which exception was handled.

Multiple Try Blocks

You have many choices about setting up try blocks. For example, you could handle the two function calls individually, placing each within its own try block. That allows you to program a different response for the two possible exceptions, as the following code shows:

while (cin >> x >> y)
{
    try {                    // try block #1
        z = hmean(x,y);
    }                        // end of try block #1
    catch (const char * s)   // start of catch block #1
    {
        cout << s << "\n";
         out << "Enter a new pair of numbers: ";
         ontinue;
    }                        // end of catch block #1
    cout << "Harmonic mean of " << x << " and " << y
         << " is " << z << "\n";
    try {                   // try block #2
        z = gmean(x,y);
    }                       // end of try block #2
    catch (const char * s)  // start of catch block #2
    {
        cout << s << "\n";
        cout << "Data entry terminated!\n";
        break;
    }                       // end of catch block #2
    cout << "Enter next set of numbers <q to quit>: ";
}

Another possibility is to nest try blocks, as the next example shows:

try {                           // outer try block
    while (cin >> x >> y)
    {
    try {                       // inner try block
        z = hmean(x,y);
    }                           // end of inner try block
    catch (const char * s)      // inner catch block
    {
    cout << s << "\n";
        cout << "Enter a new pair of numbers: ";
        continue;
    }                           // end of inner catch block
        cout << "Harmonic mean of " << x << " and " << y
             << " is " << z << "\n";
        cout << "Geometric mean of " << x << " and " << y
             << " is " << gmean(x,y) << "\n";
        cout << "Enter next set of numbers <q to quit>: ";
   }
}                               // end of outer try block
catch (const char * s)          // outer catch block
{
    cout << s << "\n";
    cout << "Sorry, you don't get to play any more. ";
}                                // end of outer catch block

Here an exception thrown by hmean() gets caught by the inner exception handler, which allows the loop to continue. But an exception thrown by gmean() gets caught by the outer exception handler, which terminates the loop.

Unwinding the Stack

Suppose a try block doesn't contain a direct call to a function throwing an exception but that it calls a function that calls a function that throws an exception. Execution still jumps from the function in which the exception is thrown to the function containing the try block and handlers. Doing so involves unwinding the stack, which we'll discuss now.

First, let's look at how C++ normally handles function calls and returns. C++ typically handles function calls by placing information on a stack (Chapter 9, "Memory Models and Name spaces"). In particular, a program places the address of a calling function instruction (a return address) on the stack. When the called function completes, the program uses this address to determine where to continue with program execution. Also, the function call places any function arguments on the stack, where they are treated as automatic variables. If the called function creates any new automatic variables, they, too, are added to the stack. If a called function calls another function, its information is added to the stack, and so on. When a function terminates, program execution passes to the address stored when the function was called, and the top of the stack is freed. Thus a function normally returns to the function that called it, and so on, with each function liberating its automatic variables as it terminates. If an automatic variable is a class object, then the class destructor, if any, is called.

Now suppose a function terminates via an exception throw instead of via a return call. Again, the program frees memory from the stack. But instead of stopping at the first return address on the stack, the program continues freeing the stack until it reaches a return address that resides in a try block (see Figure 15.3). Control then passes to the exception handlers at the end of the block rather than to the first statement following the function call. This is the process called unwinding the stack. One very important feature of the throw mechanism is that, just as with function returns, the class destructors are called for any automatic class objects on the stack. However, a function return just processes objects put on the stack by that function, while the throw statement processes objects put on the stack by the entire sequence of function calls between the try block and the throw. Without the unwinding-the-stack feature, a throw would leave destructors uncalled for automatic class objects placed on the stack by intermediate function calls.

Figure 15.3. throw versus return.

graphics/15fig03.gif

Listing 15.11 provides an example of unwinding the stack. In it, main() calls details(), and details() calls hmean(). When hmean() throws an exception, control returns all the way to main(), where it is caught. In the process, the automatic variables representing the arguments to hmean() and details() are freed.

Listing 15.11 error5.cpp
//error5.cpp
#include <iostream>
using namespace std;
double hmean(double a, double b) throw(const char *);
void details(double a, double b) throw(const char *);
int main()
{
    double x, y;

    cout << "Enter two numbers: ";
    try {
        while (cin >> x >> y)
            details(x,y);
    }
    catch (const char * s)
    {
        cout << s << "\n";
        cout << "Sorry, you can't play anymore. ";
    }
    cout << "Bye!\n";
    return 0;
}

void details(double a, double b) throw(const char *){
    cout << "Harmonic mean of " << a << " and " << b
        << " is " << hmean(a,b) << "\n";
    cout << "Enter next set of numbers <q to quit>: ";
}

double hmean(double a, double b) throw(const char *)
{
    if (a == -b)
        throw "bad hmean() arguments: a = -b not allowed";
    return 2.0 * a * b / (a + b);
}

Here's a sample run; note that throwing the exception skips directly to main(), keeping details() from displaying the text that otherwise would have been displayed had hmean() terminated normally.

Enter two numbers: 3 15
Harmonic mean of 3 and 15 is 5
Enter next set of numbers <q to quit>: 20 -20
bad hmean() arguments: a = -b not allowed
Sorry, you don't get to play any more. Bye!
More Options

You can set up a handler to catch any kind of exception. Also, if a try block is nested, you can have its handlers pass control on up to the handlers for the containing try block.

To catch any exception, use the ellipses for the exception type:

catch (...) { // statements }

To pass control to a containing try block, use throw without a following exception:

catch (const char * s)
{
      cout << "Exception caught in inner loop.\n";
      throw;  // send to containing try block
}

This sample, for example, prints a message, then passes control to the containing try block, where the program once again will look for a handler matching the original thrown exception.

Note that there is more than one way for one try block to contain another. One way is to nest one within another, as discussed earlier. Another way is for one try block to contain a function call that invokes a function containing a try block. In the first case, the preceding sample code would pass control to the outer try block. In the second case, the sample code would cause the program to unwind the stack to find the next try block.

Exceptions and Classes

Exceptions aren't just for handling error conditions in regular functions. They also can be part of a class design. For example, a constructor could throw an exception if a call to the new operator fails. Or the overloaded [] operator for an array class can throw an exception if an index is out of range. Often it's useful if the exception can carry information with it, such as the value of an invalid index. One could use, say, a type int exception in that case, but it's more useful to throw an exception that's an object. The type of object will help identify the source of the exception. The earlier example with hmean() and qmean() had the problem that both went through the same type (char char *) of exception, making it cumbersome to set up catch blocks that discriminate between the two. By using objects, you can design a different type object for each exception you wish to catch. And the object itself can carry the needful information.

Tip

graphics/bulb.gif

If you have a function throw an exception, define an exception class to be used as the type of exception thrown.

In fact, the usual practice in using exceptions is to throw objects as exceptions and to catch them by reference:

class problem {...};
...
void super() throw (problem &)
{
    ...
    if (oh_no)
    {
        problem oops(); // construct object
        throw oops;     // throw it
    ...
}
...
try {
    super();
}
catch(problem & p)
{
...
}

Incidentally, while the throw-catch mechanism is much like function argument passing, there are a few differences. For example, the compiler always creates a temporary copy when throwing an exception, so in the preceding sample code, p would refer to a copy of oops rather than oops. That's a good thing, because oops no longer exists after super() terminates. Often it is simpler to combine construction with the throw:

throw problem();     // construct and throw type problem object

When exceptions refer to class processes, it's often useful if the exception type is defined as a nested class. Not only does this make the exception type indicate the class originating an exception, it helps prevent name conflicts. For example, suppose you have a class called ArrayDbE in which you publicly declare another class called BadIndex. If the [] operator finds a bad index value, it can throw an exception of type BadIndex. Then a handler for this exception would look like this:

catch (ArrayDbE::BadIndex &) { ...}

The ArrayDbE:: qualifier identifies BadIndex as being declared in the ArrayDbE class. It also informs a reader that this handler is intended for exceptions generated by ArrayDbE objects. The BadIndex name gives the reader a pretty good idea as to the nature of the exception. This sounds rather attractive, so let's develop the idea. In particular, let's add exceptions to the ArrayDb class first developed in Chapter 14, "Reusing Code in C++."

To the header file, add an exception BadIndex class, that is, a class defining objects to be thrown as exceptions. As outlined earlier, it will be used for bad index values, and it will hold the value of the bad index. Note that the nested class declaration just describes the class; it doesn't create objects. The class methods will create objects of this class if they throw exceptions of the BadIndex type. Also note that the nested class is public. This allows the catch blocks to have access to the type. Listing 15.12 shows the new header file. The rest of the definition, aside from changing the class name, is the same as the definition of ArrayDb in Chapter 14, except that it qualifies the method prototypes to indicate which exceptions they can throw. That is, it replaces

virtual double & operator[](int i);

with

virtual double & operator[](int i) throw(ArrayDbe::BadIndex &);

and so on. (As with ordinary function arguments, it's usually better to pass references instead of objects when throwing exceptions.)

Listing 15.12 arraydbe.h
// arraydbe.h -- define array class with exceptions
#ifndef ARRAYDBE_H_
#define ARRAYDBE_H_
#include <iostream>
using namespace std;

class ArrayDbE
{
private:
    unsigned int size;  // number of array elements
protected:
    double * arr;       // address of first element
public:
    class BadIndex      // exception class for indexing problems
    {
    private:
        int badindex;   // problematic index value
     public:
        BadIndex(int i) : badindex(i) {}
        virtual void Report() const;
    };
    ArrayDbE();                          // default constructor
    // create an ArrayDbE of n elements, set each to val
    ArrayDbE(unsigned int n, double val = 0.0);
    // create an ArrayDbE of n elements, initialize to array pn
    ArrayDbE(const double * pn, unsigned int n);
    // copy constructor
    ArrayDbE(const ArrayDbE & a);
    virtual ~ArrayDbE();                 // destructor
    unsigned int ArSize() const;         // returns array size
    double Average() const;              // return array average
// overloaded operators
        // array indexing, allowing assignment
    virtual double & operator[](int i) throw(ArrayDbE::BadIndex &);
        // array indexing (no =)
    virtual const double & operator[](int i) const throw(ArrayDbE::BadIndex &);
    ArrayDbE & operator=(const ArrayDbE & a);
    friend ostream & operator<<(ostream & os, const ArrayDbE & a);
} ;

#endif

Next, you must provide the class methods. These are the same methods used in Chapter 12 with the addition of some exception throwing. Because the overloaded [] operators throw exceptions instead of calling the exit() function, the program no longer needs to include the cstdlib file. Listing 15.13 shows the result.

Listing 15.13 arraydbe.cpp
// arraydbe.cpp -- ArrayDbE class methods
#include <iostream>
using namespace std;
#include "arraydbe.h"

// BadIndex method
void ArrayDbE::BadIndex::Report() const
{
    cerr << "Out of bounds index value: " <<  badindex << endl;
}

// ArrayDbE methods
// default constructor -- no arguments
ArrayDbE::ArrayDbE()
{
   arr = NULL;
   size = 0;
}

// constructs array of n elements, each set to val
ArrayDbE::ArrayDbE(unsigned int n, double val)
{
    arr = new double[n];
    size = n;
    for (int i = 0; i < size; i++)
       arr[i] = val;
}

// initialize ArrayDbE object to a non-class array
ArrayDbE::ArrayDbE(const double *pn, unsigned int n)
{
    arr = new double[n];
    size = n;
    for (int i = 0; i < size; i++)
       arr[i] = pn[i];
}

// initialize ArrayDbE object to another ArrayDbE object
ArrayDbE::ArrayDbE(const ArrayDbE & a)
{
    size = a.size;
    arr = new double[size];
    for (int i = 0; i < size; i++)
       arr[i] = a.arr[i];
}

ArrayDbE::~ArrayDbE()
{
   delete [] arr;
}

double ArrayDbE::Average() const
{
    double sum = 0;
    int i;
    int lim = ArSize();
    for (i = 0; i < lim; i++)
        sum += arr[i];
    if (i > 0)
        return sum / i;
    else
    {
        cerr << "No entries in score array\n";
        return 0;
    }
}
// return array size
unsigned int ArrayDbE::ArSize() const
{
   return size;
}

// let user access elements by index (assignment allowed)
double & ArrayDbE::operator[](int i) throw(ArrayDbE::BadIndex &)
{
    // check index before continuing
    if (i < 0 || i >= size)
        throw BadIndex(i);
    return arr[i];
}

// let user access elements by index (assignment disallowed)
const double & ArrayDbE::operator[](int i)const throw(ArrayDbE::BadIndex &)
{
    // check index before continuing
    if (i < 0 || i >= size)
        throw BadIndex(i);
    return arr[i];
}

// define class assignment
ArrayDbE & ArrayDbE::operator=(const ArrayDbE & a)
{
    if (this == &a)      // if object assigned to self,
        return *this;    // don't change anything
    delete arr;
    size = a.size;
    arr = new double[size];
    for (int i = 0; i < size; i++)
       arr[i] = a.arr[i];
    return *this;
}

// quick output, 5 values to a line
ostream & operator<<(ostream & os, const ArrayDbE & a)
{
   int i;
   for (i = 0; i < a.size; i++)
   {
       os << a.arr[i] << " ";
       if (i % 5 == 4)
           os << "\n";
   }
   if (i % 5 != 0)
       os << "\n";
   return os;
}

Note that the exceptions now are objects instead of strings. Also note that these exception throws use the exception class constructor to create and initialize the exception objects:

if (i < 0 || i >= size)
       throw BadIndex(i);    // create, initialize a BadIndex object

What about catching this kind of exception? The exceptions are objects, not character strings, so the catch block has to reflect that fact. Also, because the exception is a nested type, the code needs to use the scope resolution operator.

try {
      ...
}
catch(ArrayDbE::BadIndex &) {
      ...
}

Listing 15.14 provides a short program demonstrating the process.

Listing 15.14 exceptar.cpp
// exceptar.cpp -- use the ArrayDbE class
// Compile with arraydbe.cpp
#include <iostream>
using namespace std;
#include "arraydbe.h"

const int Players = 5;
int main()
{
    try {
        ArrayDbE Team(Players);
        cout << "Enter free-throw percentages for your 5 "
                "top players as a decimal fraction:\n";
        int player;
        for (player = 0; player < Players; player++)
        {
             cout << "Player " << (player + 1) << ": % = ";
             cin >> Team[player];
        }
        cout.precision(1);
        cout.setf(ios_base::showpoint);
        cout.setf(ios_base::fixed,ios_base::floatfield);
        cout << "Recapitulating, here are the percentages:\n";
        for (player = 0; player <= Players; player++)
            cout << "Player #" << (player + 1) << ": "
                    << 100.0 * Team[player] << "%\n";
    }                                   // end of try block
    catch (ArrayDbE::BadIndex & bi)     // start of handler
    {
        cout << "ArrayDbE exception:\n";
        bi.Report();
    }                                   // end of handler
    cout << "Bye!\n";
   return 0;
}

Compatibility Note

graphics/hands.gif

Some compilers may not recognize the new form ios_base:: and will require ios:: instead.

Note the second for loop deliberately exceeds the array bounds, triggering an exception. Here is a sample run:

Enter free-throw percentages for your 5 top players as a decimal fraction:
Player 1: % = 0.923
Player 2: % = 0.858
Player 3: % = 0.821
Player 4: % = 0.744
Player 5: % = 0.697
Recapitulating, here are the percentages:
Player #1: 92.3%
Player #2: 85.8%
Player #3: 82.1%
Player #4: 74.4%
Player #5: 69.7%
ArrayDbE exception:
Out of bounds index value: 5
Bye!

Because the loop is inside the try block, throwing the exception terminates the loop as control passes to the second catch block following the try block.

By the way, remember that variables defined in a block, including a try block, are local to the block. For example, the variable player is undefined once program control passes beyond the try block in Listing 15.15.

Exceptions and Inheritance

Inheritance interacts with exceptions in a couple of ways. First, if a class has publicly nested exception classes, a derived class inherits those exception classes. Second, you can derive new exception classes from existing ones. We'll look at both these possibilities in the next example.

First, derive a LimitArE class from the ArrayDbE class. The LimitArE class will allow for array indexing to begin with values other than 0. This can be accomplished by storing a value representing the starting index and by redefining the functions. Internally, the array indexing still will begin with 0. But if, say, you specify 1900 as the starting index, the operator[]() method will translate an external index of 1908 to an internal index 1908?900, or 8. Listing 15.15 shows the details.

The BadIndex exception declared in the ArrayDbE class stored the offending index value. With variable index limits, it would be nice if the exception also stored the correct range for indices. You can accomplish this by deriving a new exception class from BadIndex:

class SonOfBad : public ArrayDbE::BadIndex
{
private:
    int l_lim;
    int u_lim;
public:
    SonOfBad(int i, int l, int u) : BadIndex(i),
            l_lim(l), u_lim(u) {}
    void Report() const;   // redefines Report()
} ;

You can nest the SonOfBad declaration in the LimitArE declaration. Listing 15.15 shows the result.

Listing 15.15 limarre.h
// limarre.h -- LimitArE class with exceptions

#ifndef LIMARRE_H_
#define LIMARRE_H_

#include "arraydbe.h"

class LimitArE : public ArrayDbE
{
public:
    class SonOfBad : public ArrayDbE::BadIndex
    {
    private:
        int l_lim;
        int u_lim;
    public:
        SonOfBad(int i, int l, int u) : BadIndex(i),
                l_lim(l), u_lim(u) {}
        void Report() const;
    } ;
private:
    unsigned int low_bnd;          // new data member
protected:
    // handle bounds checking
    virtual void ok(int i) const throw(ArrayDbE::BadIndex &);
public:
// constructors
    LimitArE() : ArrayDbE(), low_bnd(0) {}
    LimitArE(unsigned int n, double val = 0.0)
                : ArrayDbE(n,val), low_bnd(0) {}
    LimitArE(unsigned int n, int lb, double val = 0.0)
                : ArrayDbE(n, val), low_bnd(lb) {}
    LimitArE(const double * pn, unsigned int n)
                : ArrayDbE(pn, n), low_bnd(0) {}
    LimitArE(const ArrayDbE & a) : ArrayDbE(a), low_bnd(0) {}
// new methods
    void new_lb(int lb) { low_bnd = lb;}     // reset lower bound
    int lbound() { return low_bnd;}          // return lower bound
    int ubound() { return ArSize() + low_bnd - 1;}  // upper bound
// redefined operators
    double & operator[](int i) throw(ArrayDbE::BadIndex &);
    const double & operator[](int i) const throw(ArrayDbE::BadIndex &);
} ;
#endif

This design moves index checking from the overloaded [] methods to an ok() method that's called by the overloaded [] methods. C++ requires that the throw specification for virtual method redefined in the derived class match the throw specification in the base class. However, a base class reference can refer to a derived object. Therefore, although the LimitArE::ok() method throws a SonOfBad exception, the throw specifiers can list the ArrayDbe::BadIndex class. Listing 15.16 shows the class methods.

Listing 15.16 limarre.cpp
// limarre.cpp
#include "limarre.h"
#include <iostream>
using namespace std;

// BadIndex method
void LimitArE::SonOfBad::Report() const
{
    ArrayDbE::BadIndex::Report();
    cerr << "Index should be in the range " << l_lim
         << " through " << u_lim << endl;
}

//limarre methods
// private method
   // lower bound for array index is now low_bnd, and
   // upper bound is now low_bnd + size - 1
void LimitArE::ok(int i) const throw(ArrayDbE::BadIndex &)
{
    unsigned long size = ArSize();
    if (i < low_bnd || i >= size + low_bnd)
        throw SonOfBad(i, low_bnd, low_bnd + size - 1);
}

// redefined operators
double & LimitArE::operator[](int i) throw(ArrayDbE::BadIndex &)
{
   ok(i);
   return arr[i - low_bnd];
}

const double & LimitArE::operator[](int i) const throw(ArrayDbE::BadIndex &)
{
   ok(i);
   return arr[i - low_bnd];
}
}

Suppose you have a program with both ArrayDbE and LimitArE objects. Then you would want a try block that catches the two possible exceptions: BadIndex and SonOfBad. You can do that by following the try block with two consecutive catch blocks:

try {
    LimitArE income(Years, FirstYear);
    ArrayDbE busywork(Years);
    ...
}   // end of try block
catch (LimitArE::SonOfBad & bi)   // 1st handler
{
    ...
}
catch (ArrayDbE::BadIndex & bi)   // 2nd handler
{
    ...
}

When there is a sequence of catch blocks, a program attempts to match a thrown exception to the first catch block, then the second catch block, and so on. As soon as there's a match, the program executes that catch block. Providing the code in the catch block doesn't terminate the program or generate another throw; the program jumps to the statement following the final catch block after completing any one catch block in the sequence.

This particular sequence of catch blocks has an interesting property梐 catch block with a BadIndex reference can catch either a BadIndex exception or a SonOfBad exception. That's because a base class reference can refer to a derived object. However, a catch block with a SonOfBad reference can't catch a BadIndex object. That's because a derived object reference can't refer to a base class object without an explicit type cast. This state of affairs suggests placing the SonOfBad catch block above the BadIndex catch block. That way, the SonOfBad catch block will catch a SonOfBad exception while passing a BadIndex exception on to the next catch block. The program in Listing 15.17 illustrates this approach.

Listing 15.17 excptinh.cpp
// excptinh.cpp -- use the ArrayDbE and LimitArE classes
// Compile with arraydbe.cpp, limarre.cpp
#include <iostream>
using namespace std;
#include "arraydbe.h"
#include "limarre.h"

const int Years = 4;
const int FirstYear = 2001;
int main()
{
    int year;
    double total = 0;
    try {
        LimitArE income(Years, FirstYear);
        ArrayDbE busywork(Years);
        cout << "Enter your income for the last " << Years
                << " years:\n";
        for (year = FirstYear; year < FirstYear + Years; year++)
        {
             cout << "Year " << year << ": $";
             cin >> income[year];
             busywork[year - FirstYear] = 0.2 * income[year];
        }
        cout.precision(2);
        cout.setf(ios::showpoint);
        cout.setf(ios::fixed,ios::floatfield);
        cout << "Recapitulating, here are the figures:\n";
        for (year = FirstYear; year <= FirstYear + Years; year++)
        {
            cout << year << ": $" << income[year] << "\n";
            total += income[year];
        }
        cout << "busywork values: " << busywork;
    }   // end of try block
    catch (LimitArE::SonOfBad & bi)   // 1st handler
    {
        cout << "LimitArE exception:\n";
        bi.Report();
    }
    catch (ArrayDbE::BadIndex & bi)   // 2nd handler
    {
        cout << "ArrayDbE exception:\n";
        bi.Report();
    }
    cout << "Total income for " << (year - FirstYear)
            << " years is $" << total << ".\n";
    cout << "Bye!\n";
    return 0;
}

Compatibility Note

graphics/hands.gif

Some compilers may not recognize the new form ios_base:: and will require ios:: instead.

Here is a sample run:

Enter your income for the last 4 years:
Year 2001: $54000
Year 2002: $28000
Year 2003: $92000
Year 2004: $37000
Recapitulating, here are the figures:
2001: $54000.00
2002: $28000.00
2003: $92000.00
2004: $37000.00
LimitArE exception:
Out of bounds index value: 2005
Index should be in the range 2001 through 2004
Total income for 4 years is $211000.00.Bye!

The SonOfBad exception terminated execution of the try block and transferred execution to the second catch block. Once the program finished processing the catch block, it jumped to the first statement following the catch blocks.

The following tip summarizes this example's main lesson.

Tip

graphics/bulb.gif

If you have an inheritance hierarchy of exception classes, arrange the order of the catch blocks so the most derived class exception is caught first and the base class exception is caught last.

The exception Class

The main intent for C++ exceptions is to provide language-level support for designing fault-tolerant programs. That is, exceptions make it easier to incorporate error handling into a program design rather than tacking on some more rigid form of error handling as an afterthought. The flexibility and relative convenience of exceptions should encourage programmers to integrate fault handling into the program design process, if appropriate. In short, exceptions are the kind of feature that, like classes, can modify your approach to programming.

Newer C++ compilers are incorporating exceptions into the language. For example, the exception header file (formerly exception.h or except.h) defines an exception class that C++ uses as a base class for other exception classes used to support the language. Your code, too, can throw an exception object or use the exception class as a base class. One virtual member function is named what(), and it returns a string, the nature of which is implementation-dependent. But if you derive a class, you then can choose what string you want returned. For example, you could replace the BadIndex declaration with one based on the exception class. Because the contents of the exception header file are part of the std namespace, you can make them available with a using directive:

#include <exception>
using namespace std;
class ArrayDbE
{
private:
    unsigned int size;  // number of array elements
protected:
    double * arr;       // address of first element
public:
    class OutOfBounds : public exception
    {
    public:
        OutOfBounds() : exception() {};
        const char * what() {return "Array limit out of bounds\n";}
    };
   ...
};

Then you could have the overloaded [] methods throw an OutOfBounds exception, have a program using the class catch that type of exception, and use the what() method to identify the problem:

catch (ArrayDbE::OutOfBounds & ob)   //  handler
{
    cout << "ArrayDbE exception: "
            << ob.what() << endl;
}

By the way, instead of making all the exception class declarations available with a using directive, you could use the scope resolution operator:

class OutOfBounds : public std::exception

The bad_alloc Exception and new

C++ gives implementations two choices for handling memory allocation problems when using new. The first choice, and once the only choice, is to have new return the null pointer if it can't satisfy a memory request. The second choice is to have new throw a bad_alloc exception. The new header (formerly new.h) includes a declaration for the bad_alloc class, which is publicly derived from the exception class. An implementation may offer just one choice or, perhaps by using a compiler switch or some other method, let you choose the approach you prefer.

Listing 15.18 straddles the issue by trying both approaches. If the exception is caught, the program displays the implementation-dependent message returned by the inherited what() method and terminates early. Otherwise, it proceeds to see if the return value was the null pointer. (The purpose here is to show the two ways to check for memory allocation errors, not to suggest that a typical program would actually use both methods.)

Listing 15.18 newexcp.cpp
// newexcp.cpp -- the bad_alloc exception
#include <iostream>
using namespace std;
#include <new>
#include <cstdlib>

struct Big
{
    double stuff[2000];
};

int main()
{
    Big * pb;
    try {
        cout << "Trying to get a big block of memory:\n";
        pb = new Big[10000];
        cout << "Got past the new request:\n";
    }
    catch (bad_alloc & ba)
    {
        cout << "Caught the exception!\n";
        cout << ba.what() << endl;
        exit(1);
    }
    if (pb != 0)
    {
        pb[0].stuff[0] = 4;
        cout << pb[0].stuff[0] << endl;
    }
    else
        cout << "pb is null pointer\n";
    delete [] pb;
    return 0;
}

When Exceptions Go Astray

After an exception has been thrown, it has two opportunities to cause problems. First, if it is thrown in a function having an exception specification, it has to match one of the types in the specification list. If it doesn't, the unmatched exception is branded an unexpected exception, and, by default, it causes the program to abort. If the exception passes this first hurdle (or avoids it because the function lacks an exception specification), it then has to be caught. If it isn't, which can happen if there is no containing try block or no matching catch block, the exception is branded an uncaught exception, and, by default, it causes the program to abort. However, you can alter a program's response to unexpected and uncaught exceptions. Let's see how, beginning with uncaught exceptions.

An uncaught exception doesn't initiate an immediate abort. Instead, the program first calls a function called terminate(). By default, terminate() calls the abort() function. You can modify the behavior of terminate() by registering a function that terminate() should call instead of abort(). To do this, call the set_terminate() function. Both set_terminate() and terminate() are declared in the exception header file:

typedef void (*terminate_handler)();
terminate_handler set_terminate(terminate_handler f) throw();
void terminate();

The set_terminate() function takes, as its argument, the name of a function (that is, its address) having no arguments and the void return type. It returns the address of the previously registered function. If you call the set_terminate() function more than once, terminate() calls the function set by the most recent call to set_terminate().

Let's look at an example. Suppose you want an uncaught exception to cause a program to print a message to that effect and then call the exit() function, providing an exit status value of 5. First, include the exception header file. You can make its declarations available with a using directive or appropriate using declarations, or you can use the std:: qualifier.

#include <exception>
using namespace std;

Next, design a function that does the two required actions and has the proper prototype:

void myQuit()
{
    cout << "Terminating due to uncaught exception\n";
    exit(5);
}

Finally, at the start of your program, designate this function as your chosen termination action:

set_terminate(myQuit);

Next, let's look at unexpected exceptions. By using exception specifications for a function, you provide the means for users of the functions to know which exceptions to catch. That is, suppose you have the following prototype:

double Argh(double, double) throw(exception &);

Then you might use the function this way:

try {
    x = Argh(a, b);
}
catch(exception & ex)
{
    ...
}

It's good to know which exceptions to catch; recall that an uncaught exception, by default, aborts the program.

However, there's a bit more to the story. In principle, the exception specification should include exceptions thrown by functions called by the function in question. For example, if Argh() calls a Duh() function that can throw a retort object exception, then retort should appear in the Argh() exception specification as well as in the Duh() exception specification.

Unless you write all the functions yourself and are careful, there's no guarantee this will get done correctly. You might, for example, use an older commercial library whose functions don't have exception specifications. This suggests looking more closely at what happens if a function does throw an exception not in its exception specification.

The behavior is much like that for uncaught exceptions. If there is an unexpected exception, the program calls the unexpected() function. (You didn't expect the unexpected() function? No one expects the unexpected() function!) This function, in turn, calls terminate(), which, by default, calls abort(). Just as there is a set_terminate() function that modifies the behavior of terminate(), there is a set_unexpected() function that modifies the behavior of unexpected(); these new functions also are declared in the exception header file:

typedef void (*unexpected_handler)();
unexpected_handler set_unexpected(unexpected_handler f) throw();
void unexpected();

However, the behavior of the function you supply set_unexpected() is more regulated than that of a function for set_terminate(). In particular, the unexpected_handler function has the following choices:

The result of throwing an exception (the second choice above) depends upon the exception thrown by the replacement unexpected_handler function and the original exception specification for the function that threw the unexpected type:

In short, if you'd like to catch all exceptions, expected or otherwise, you can do something along these lines:

First, make sure the exception header file declarations are available:

#include <exception>
using namespace std;

Next, design a replacement function that converts unexpected exceptions to the bad_exception type and that has the proper prototype:

void myUnexpected()
{
    throw std::bad_exception();  //or just throw;
}

Just using throw without an exception rethrows the original exception. However, it will be replaced with a bad_exception object if the exception specification includes that type.

Next, at the start of your program, designate this function as your chosen unexpected exception action:

set_unexpected(myUnexpeced);

Finally, include the bad_exception type in exception specifications and catch block sequences:

double Argh(double, double) throw(exception &, bad_exception &);
...
try {
    x = Argh(a, b);
}
catch(exception & ex)
{
    ...
}
catch(bad_exception & ex)
{
    ...
}

Exception Cautions

From the preceding discussion of using exceptions, you might gather (and gather correctly) that exception handling should be designed into a program rather than tacked on. It has some disadvantages. For example, using exceptions adds to the size and subtracts from the speed of a program. Exception specifications don't work well with templates, because template functions might throw different kinds of exceptions depending upon the particular specialization used. Exceptions and dynamic memory allocation don't always work that well together.

Let's look a little further at dynamic memory allocation and exceptions. First, consider the following function:

void test1(int n)
{
    String mesg("I'm trapped in an endless loop");
    ...
    if (oh_no)
        throw exception();
    ...
    return;
}

The String class, recall, used dynamic memory allocation. Normally, the String destructor for mesg would be called when the function reached return and terminated. Thanks to stack unwinding, the throw statement, even though it terminates the function prematurely, still allows the destructor to be called. So here memory is managed properly.

Now consider this function:

void test2(int n)
{
    double * ar = new double[n];
    ...
    if (oh_no)
        throw exception();
    ...
    delete [] ar;
    return;
}

Here there is a problem. Unwinding the stack removes the variable ar from the stack. But the premature termination of the function means the delete [] statement at the end of the function is skipped. The pointer is gone, but the memory block it pointed to is still intact and inaccessible. In short, there is a memory leak.

The leak can be avoided. For example, you can catch the exception in the same function that throws it, put some cleanup code into the catch block, then rethrow the exception:

void test3(int n)
{
    double * ar = new double[n];
    ...
    try {
        if (oh_no)
            throw exception();
    }
    catch(exception & ex)
    {
        delete [] ar;
        throw;
    }
    ...
    delete [] ar;
    return;
}

However, this clearly enhances the opportunities for oversights and other errors. Another solution is to use the auto_ptr template discussed in Chapter 16, "The String Class and the Standard Template Library."

In short, while exception handling is extremely important for some projects, it does have costs in programming effort, program size, and program speed. Also, compiler exception support and user experience have not yet reached the mature level. So you might want to use this feature with moderation.

Real World Note: Exception Handling

graphics/common.gif

In modern libraries, exception handling can appear to reach new levels of complexity梞uch of it due to undocumented or poorly documented exception handling routines. Anyone familiar with the use of a modern operating system has surely seen the errors and problems caused by unhandled exceptions. The programmers behind these errors often face an uphill battle, learning the complexity that lies within the libraries: what exceptions are thrown, why and when they occur, and how to handle them.

The novice programmer quickly finds the research battle to understand exception handling in a library is as difficult as the struggle to learn the language itself; modern libraries can contain routines and paradigms as alien and difficult as any C++ syntax detail. Exposure to and understanding the intricacies of your libraries and classes is, for good software, as necessary as the time you spend learning C++ itself. The exception and error-handling details you decipher from your libraries' documentation and source code will always serve you, and your software, in good stead.

RTTI

RTTI is short for runtime type information. It's one of the more recent additions to C++ and isn't supported by many older implementations. Other implementations may have compiler settings for turning RTTI on and off. The intent of RTTI is to provide a standard way for a program to determine the type of object during runtime. Many class libraries have already provided ways to do so for their own class objects, but in the absence of built-in support in C++, each vendor's mechanism typically is incompatible with those of other vendors. Creating a language standard for RTTI should allow future libraries to be compatible with each other.

What's It For?

Suppose you have a hierarchy of classes descended from a common base. You can set a base class pointer to point to an object of any of the classes in this hierarchy. Next, you call a function that, after processing some information, selects one of these classes, creates an object of that type, and returns its address, which gets assigned to a base class pointer. How can you tell what kind of object it points to?

Before answering this question, let's think about why you would want to know the type. Perhaps you want to invoke the correct version of a class method. If that's the case, you don't really need to know the object type as long as that function is a virtual function possessed by all members of the class hierarchy. But it could be that a derived object has an uninherited method. In that case, only some objects could use the method. Or maybe, for debugging purposes, you would like to keep track of which kinds of objects were generated. For these last two cases, RTTI provides an answer.

How Does It Work?

C++ has three components supporting RTTI:

You can use RTTI only with a class hierarchy having virtual functions. The reason for this is that these are the only class hierarchies for which you should be assigning the addresses of derived objects to base class pointers.

Caution

graphics/tnt.gif

RTTI works only for classes with virtual functions.

Let's examine the three components of RTTI.

The dynamic_cast Operator

The dynamic_cast operator is intended to be the most heavily used RTTI component. It doesn't answer the question of what type of object a pointer points to. Instead, it answers the question of whether you can safely assign the address of the object to a pointer of a particular type. Let's see what that means. Suppose you have the following hierarchy:

class Grand {  // has virtual methods} ;
class Superb : public Grand {  ... } ;
class Magnificent : public Superb {  ... } ;

Next, suppose you have the following pointers:

Grand * pg = new Grand;
Grand * ps = new Superb;
Grand * pm = new Magnificent;

Finally, consider the following type casts:

Magnificent * p1 = (Magnificent *) pm;        // #1
Magnificent * p2 = (Magnificent *) pg;        // #2
Superb * p3 = (Magnificent *) pm;             // #3

Which of the previous type casts are safe? Depending upon the class declarations, all of them could be safe, but the only ones guaranteed to be safe are the ones in which the pointer is the same type as the object or else a direct or indirect base type for the object. For example, type cast #1 is safe because it sets a type Magnificent pointer to point to a type Magnificent object. Type cast #2 is not safe because it assigns the address of a base object (Grand) to a derived class (Magnificent) pointer. Thus, the program would expect the base class object to have derived class properties, and that, in general, is false. A Magnificent object, for example, might have data members that a Grand object would lack. Type cast #3, however, is safe, because it assigns the address of a derived object to a base class pointer. That is, public derivation promises that a Magnificent object also is a Superb object (direct base) and a Grand object (indirect base). Thus, it's safe to assign its address to pointers of all three types. Virtual functions ensure that using pointers of any of the three types to a Magnificent object will invoke Magnificent methods.

Note that the question of whether a type conversion is safe is both more general and more useful than the question of what kind of object is pointed to. The usual reason for wanting to know the type is so that you can know if it's safe to invoke a particular method. You don't necessarily need an exact type match to invoke a method. The type can be a base type for which a virtual version of the method is defined. The next example will illustrate this point.

First, however, let's look at the dynamic_cast syntax. The operator is used like this, where pg points to an object:

Superb pm = dynamic_cast<Superb *>(pg);

This asks the question, can the pointer pg be type cast safely (as described previously) to the type Superb *? If it can, the operator returns the address of the object. Otherwise it returns 0, the null pointer.

Remember

graphics/arrow.gif

In general, the expression

dynamic_cast<Type *>(pt)

converts the pointer pt to a pointer of type Type * if the pointed-to object (*pt) is of type Type or else derived directly or indirectly from type Type. Otherwise, the expression evaluates to 0, the null pointer.

Listing 15.19 illustrates the process. First, it defines three classes, coincidentally named Grand, Superb, and Magnificent. The Grand class defines a virtual Speak() function, which each of the other classes redefines. The Superb class defines a virtual Say() function, which Magnificent redefines (see Figure 15.4). The program defines a GetOne() function that randomly creates and initializes an object of one of these three types, then returns the address as a type Grand * pointer. (The GetOne() function simulates an interactive user making decisions.) A loop assigns this pointer to a type Grand * variable called pg, then uses pg to invoke the Speak() function. Because this function is virtual, the code correctly invokes the Speak() version appropriate to the pointed-to object:

for (int i = 0; i < 5; i++)
{
    pg = GetOne();
    pg->Speak();
    ...
Figure 15.4. The Grand family of classes.

graphics/15fig04.gif

However, you can't use this exact approach to invoke the Say() function; it's not defined for the Grand class. However, you can use the dynamic_cast operator to see if pg can be type cast to a pointer to Superb. This will be true if the object is either type Superb or Magnificent. In either case, you can invoke the Say() function safely:

if (ps = dynamic_cast<Superb *>(pg))
    ps->Say();

Recall that the value of an assignment expression is the value of its left-hand side. Thus, the value of the if condition is ps. If the type cast succeeds, ps is nonzero, or true. If the type cast fails, which it will if pg points to a Grand object, ps is zero, or false. Listing 15.19 shows the full code. (By the way, some compilers, noting that usually one uses the == operator in an if statement condition, may issue a warning about unintended assignment.)

Listing 15.19 rtti1.cpp
// rtti1.cpp -- use the RTTI dynamic_cast operator
#include <iostream>
using namespace std;
#include <cstdlib>
#include <ctime>

class Grand
{
private:
    int hold;
public:
    Grand(int h = 0) : hold(h) {}
    virtual void Speak() const { cout << "I am a grand class!\n";}
    virtual int Value() const { return hold; }
};
class Superb : public Grand
{
public:
    Superb(int h = 0) : Grand(h) {}
    void Speak() const { cout << "I am a superb class!!\n"; }
    virtual void Say() const
        {  cout << "I hold the superb value of " << Value() << "!\n";}
} ;

class Magnificent : public Superb
{
private:
    char ch;
public:
    Magnificent(int h = 0, char c = 'A') : Superb(h), ch { }
    void Speak() const { cout << "I am a magnificent class!!!\n";}
    void Say() const { cout << "I hold the character " << ch <<
               " and the integer "  << Value() << "!\n"; }
} ;

Grand * GetOne();
int main()
{
    srand(time(0));
    Grand * pg;
    Superb * ps;
    for (int i = 0; i < 5; i++)
    {
        pg = GetOne();
        pg->Speak();
        if( ps = dynamic_cast<Superb *>(pg))
            ps->Say();
    }
    return 0;
}

Grand * GetOne()    // generate one of three kinds of objects randomly
{
    Grand * p;
    switch( rand() % 3)
    {
        case 0: p = new Grand(rand() % 100);
                    break;
        case 1: p = new Superb(rand() % 100);
                    break;
        case 2: p = new Magnificent(rand() % 100, 'A' + rand() % 26);
                    break;
    }
    return p;
}

Compatibility Note

graphics/hands.gif

Even if your compiler supports RTTI, it might have that feature turned off by default, so you should check your documentation or explore the menu options.

This program illustrates an important point. You should use virtual functions when possible and RTTI only when necessary. Here is a sample output:

I am a superb class!!
I hold the superb value of 68!
I am a magnificent class!!!
I hold the character R and the integer 68!
I am a magnificent class!!!
I hold the character D and the integer 12!
I am a magnificent class!!!
I hold the character V and the integer 59!
I am a grand class!

As you can see, the Say() methods were invoked just for the Superb and the Magnificent classes.

You can use dynamic_cast with references, too. The usage is slightly different; there is no reference value corresponding to the null-pointer type, hence there's no special reference value that can be used to indicate failure. Instead, when goaded by an improper request, dynamic_cast throws a type bad_cast exception, which is derived from the exception class and defined in the typeinfo header file. Thus, the operator can be used as follows, where rg is a reference to a Grand object:

#include <typeinfo> // for bad_cast
...
try {
    Superb & rs = dynamic_cast<Superb &>(rg);
    ...
}
catch(bad_cast &){
    ...
} ;
The typeid Operator and type_info Class

The typeid operator lets you determine if two objects are the same type. Somewhat like sizeof, it accepts two kinds of arguments:

The typeid operator returns a reference to a type_info object, where type_info is a class defined in the typeinfo header file (formerly typeinfo.h). The type_info class overloads the == and != operators so that you can use these operators to compare types. For example, the expression

typeid(Magnificent) == typeid(*pg)

evaluates to the bool value true if pg points to a Magnificent object and to false otherwise. If pg happens to be a null pointer, the program will throw a bad_typeid exception. This exception type is derived from the exception class and is declared in the typeinfo header file.

The implementation of the type_info class will vary among vendors, but it will include a name() member that returns an implementation-dependent string that typically would be the name of the class. For example, the statement

cout << "Now processing type " << typeid(*pg).name() << ".\n";

displays the string defined for the class of the object to which the pointer pg points.

Listing 15.20 modifies Listing 15.19 so that it uses the typeid operator and the name() member function. Note that they are used for situations that dynamic_cast and virtual functions don't handle. The typeid test is used to select an action that isn't even a class method, so it can't be invoked by a class pointer. The name() method statement shows how the method can be used in debugging. Note that the program includes the typeinfo header file.

Listing 15.20 rtti2.cpp
// rtti2.cpp  -- use dynamic_cast, typeid, and type_info
#include <iostream>
using namespace std;
#include <cstdlib>
#include <ctime>
#include <typeinfo>

class Grand
{
private:
    int hold;
public:
    Grand(int h = 0) : hold(h) {}
    virtual void Speak() const {  cout << "I am a grand class!\n";}
    virtual int Value() const {  return hold; }
} ;

class Superb : public Grand
{
public:
    Superb(int h = 0) : Grand(h) {}
    void Speak() const { cout << "I am a superb class!!\n"; }
    virtual void Say() const
        {  cout << "I hold the superb value of " << Value() << "!\n";}
} ;

class Magnificent : public Superb
{
private:
    char ch;
public:
    Magnificent(int h = 0, char c = 'A') : Superb(h), ch { }
    void Speak() const { cout << "I am a magnificent class!!!\n";}
    void Say() const { cout << "I hold the character " << ch <<
               " and the integer "  << Value() << "!\n"; }
} ;

Grand * GetOne();
int main()
{
    srand(time(0));
    Grand * pg;
    Superb * ps;
    for (int i = 0; i < 5; i++)
    {
        pg = GetOne();
        cout << "Now processing type " << typeid(*pg).name() << ".\n";
        pg->Speak();
        if( ps = dynamic_cast<Superb *>(pg))
            ps->Say();
        if (typeid(Magnificent) == typeid(*pg))
            cout << "Yes, you're really magnificent.\n";
}
    return 0;
}

Grand * GetOne()
{
    Grand * p;

    switch( rand() % 3)
    {
        case 0: p = new Grand(rand() % 100);
                    break;
        case 1: p = new Superb(rand() % 100);
                    break;
        case 2: p = new Magnificent(rand() % 100, 'A' + rand() % 26);
                    break;
    }
    return p;
}

Here's a sample run:

Now processing type Magnificent.
I am a magnificent class!!!
I hold the character P and the integer 52!
Yes, you're really magnificent.
Now processing type Superb.
I am a superb class!!
I hold the superb value of 37!
Now processing type Grand.
I am a grand class!
Now processing type Superb.
I am a superb class!!
I hold the superb value of 18!
Now processing type Grand.
I am a grand class!
Misusing RTTI

RTTI has many vocal critics within the C++ community. They view RTTI as unnecessary, a potential source of program inefficiency, and as a possible contributor to bad programming practices. Without delving into the debate over RTTI, let's look at the sort of programming that you should avoid.

Consider the core of Listing 15.19:

Grand * pg;
Superb * ps;
for (int i = 0; i < 5; i++)
{
    pg = GetOne();
    pg->Speak();
    if( ps = dynamic_cast<Superb *>(pg))
         ps->Say();
}

By using typeid and ignoring dynamic_cast and virtual functions, you can rewrite this code as follows:

Grand * pg;
Superb * ps;
Magnificent * pm;
for (int i = 0; i < 5; i++)
{
    pg = GetOne();
    if (typeid(Magnificent) == typeid(*pg))
    {
        pm = (Magnificent *) pg;
        pm->Speak();
        pm->Say();
    }
    else if (typeid(Superb) == typeid(*pg))
    {
        ps = (Superb *) pg;
        ps->Speak();
        ps->Say();
    }
    else
        pg->Speak();
}

Not only is this uglier and longer than the original, it has the serious flaw of naming each class explicitly. Suppose, for example, that you find it necessary to derive an Insufferable class from the Magnificent class. The new class redefines Speak() and Say(). With the versionthat uses typeid to test explicitly for each type, you would have to modify the for loop code, adding a new else if section. The original version, however, requires no changes at all. The

pg->Speak();

statement works for all classes derived from Grand, and the

if( ps = dynamic_cast<Superb *>(pg))
      ps->Say();

statement works for all classes derived from Superb.

Tip

graphics/bulb.gif

If you find yourself using typeid in an extended series of if else statements, check to see whether you should have been using virtual functions and dynamic_cast.

Type Cast Operators

The C cast operator, in Bjarne Stroustrup's view, is too lax. For example, consider the following:

struct Data
{
    double data[200];
} ;

struct Junk
{
    int junk[100];
} ;
Data d = { 2.5e33, 3.5e-19, 20.2e32} ;
char * pch = (char *) (&d);   // typecast #1 ?convert to string
char ch = char (&d);          // typecast #2 - convert address to a char
Junk * pj = (Junk *) (&d);    // typecast #3 - convert to Junk pointer

First, which of these three type casts makes any sense? Unless you resort to the implausible, none of them make much sense. Second, which of these three type casts are allowed? All of them are.

Stroustrup's response to this laxity was to add four type cast operators that provide more discipline for the casting process:

dynamic_cast
const_cast
static_cast
reinterpret_cast

Instead of using a general type cast, you can select an operator suited to a particular purpose. This documents the intended reason for the type cast and gives the compiler a chance to check that you did what you thought you did.

You've already seen the dynamic_cast operator. To summarize, suppose High and Low are two classes, that ph is type High * and pl is type Low *. Then the statement

pl = dynamic_cast<Low *> ph;

assigns a Low * pointer to pl only if Low is an accessible base class (direct or indirect) to High. Otherwise, the statement assigns the null pointer to pl. In general, the operator has this syntax:

dynamic_cast < type-name > (expression)

The purpose of this operator is to allow upcasts within a class hierarchy (such type casts being safe because of the is-a relationship) and to disallow other casts.

The const_cast operator is for making a type cast with the sole purpose of changing whether a value is const or volatile or not. It has the same syntax as the dynamic_cast operator:

const_cast < type-name > (expression)

The result making such a cast is an error if any other aspect of the type is altered. That is, type_name and expression must be of the same type except they can differ in the presence or absence of const or volatile. Again, suppose High and Low are two classes:

High bar;
const High * pbar;
    ...
High * pb = const_cast<High *> (pbar);    // valid
Low * pl = const_cast<Low *> (pbar);      // invalid

The first type cast makes *pb a pointer that can be used to alter the value of the bar object; it removes the const label. The second type cast is invalid, because it also attempts to change the type from High * to Low *.

The reason for this operator is that occasionally you may have a need for a value that is constant most of the time but which can be changed occasionally. Then you can declare the value as const and use const_cast when you need to alter the value. This could be done using the general cast, but the general cast can also simultaneously change the type:

High bar;
const High * pbar;
...
High * pb = (const High *) (pbar);    // valid
Low * pl = (const Low *) (pbar);    // also valid

Because the simultaneous change of type and constantness may be an unintentional programming slip, using the const_cast operator is safer.

The const_cast is not all powerful. It can change the pointer access to a quantity, but the effect of attempting to change a quantity that is declared const is undefined. Let's clarify this statement with the short example shown in Listing 15.21.

Listing 15.21 constcast.cpp
// constcast.cpp
#include <iostream>
using std::cout;

void change(const int * pt, int n);

int main()
{
    int pop1 = 38383;
    const int pop2 = 2000;

    cout << "pop1, pop2: " << pop1 << ", " << pop2 << '\n';
    change(&pop1, -1);
    change(&pop2, -1);
    cout << "pop1, pop2: " << pop1 << ", " << pop2 << '\n';

    return 0;
}

void change(const int * pt, int n)
{
    int * pc;
    if (n < 0)
    {
        pc = const_cast<int *>(pt);
        *pc = 100;
    }
}

The const_cast operator can remove the const from const int * pt, thus allowing the compiler to accept the following statement in change():

*pc = 100;

However, because pop2 is declared as const, the compiler may protect it from any change, as is shown by the following sample output:

pop1, pop2: 38383, 2000
pop1, pop2: 100, 2000

As you can see, the calls to change() altered pop1 but not pop2. (The particular compiler used here generated a temporary copy of pop2 and assigned that address to pc, but, as mentioned, the standard says the behavior in this situation is undefined.)

The static_cast operator has the same syntax as the others:

static_cast < type-name > (expression)

It's valid only if type_name can be converted implicitly to the same type expression has, or vice versa. Otherwise, the cast is an error. Suppose High is a base class to Low and that Pond is an unrelated class. Then conversions from High to Low and Low to High are valid, but a conversion from Low to Pond is disallowed:

High bar;
Low blow;
...
High * pb = static_cast<High *> (&blow);     // valid upcast
Low * pl = static_cast<Low *> (&bar);        // valid downcast
Pond * pmer = static_cast<Pond *> (&blow);   // invalid, Pond unrelated

The first conversion is valid because an upcast can be done explicitly. The second conversion, from a base-class pointer to a derived-class pointer can't be done without an explicit type conversion. But because the type cast in the other direction can be made without a type cast, it's valid to use a static_cast for a downcast.

Similarly, because an enumeration value can be converted to an integral type without a type cast, an integral type can be converted to an enumeration value with static_cast. Similarly, you can use static_cast to convert double to int, float to long, and the various other numeric conversions.

The reinterpret_cast operator is for inherently risky type-casts. It won't let you cast away const, but it will do other unsavory things. Sometimes a programmer has to do implementation-dependent, unsavory things, and using the reinterpret_cast operator makes it simpler to keep track of such acts. It has the same syntax as the other three:

reinterpret_cast < type-name > (expression)

Here is a sample use:

struct dat { short a; short b} ;
long value = 0xA224B118;
dat * pd = reinterpret_cast< dat *) (&value);
cout << pd->a;   // display first 2 bytes of value

Typically, such casts would be used for low-level, implementation-dependent programming and would not be portable. For example, this code sample produces different output on an IBM-compatible than it does on a Macintosh because the two systems store the bytes in multibyte integer types in opposite orders.

Summary

Friends allow you to develop a more flexible interface for classes. A class can have other functions, other classes, and member functions of other classes as friends. In some cases, you may need to use forward declarations and to exert care in the ordering of class declarations and methods in order to get friends to mesh properly.

Nested classes are classes declared within other classes. Nested classes facilitate the design of helper classes that implement other classes but needn't be part of a public interface.

The C++ exception mechanism provides a flexible way to deal with awkward programming events such as inappropriate values, failed I/O attempts, and the like. Throwing an exception terminates the function currently executing and transfers control to a matching catch block. Catch blocks immediately follow a try block, and for an exception to be caught, the function call that directly or indirectly led to the exception must be in the try block. The program thenexecutes the code in the catch block. This code may attempt to fix the problem or it can terminate the program. A class can be designed with nested exception classes that can be thrown when problems specific to the class are detected. A function can include an exception specification identifying those exceptions that can be thrown in that function. Uncaught exceptions (those with no matching catch block) by default, terminate a program. So do unexpected exceptions (those not matching an exception specification.)

The RTTI (runtime type information) features allow a program to detect the type of an object. The dynamic_cast operator is used to cast a derived class pointer to a base class pointer; its main purpose is to ensure that it's okay to invoke a virtual function call. The typeid operator returns a type_info object. Two typeid return values can be compared to determine if an object is of a specific type, and the returned type_info object can be used to obtain information about an object.

The dynamic_cast operator, along with static_cast, const_cast, and reinterpret_cast, provide safer, better-documented type casts than the general cast mechanism.

Review Questions

1:

What's wrong with the following attempts at establishing friends?

  1. class snap {
        friend clasp;
         ...
    } ;
    class clasp {  ... } ;
    
  2. class cuff {
    public:
          void snip(muff &) {  ... }
          ...
    } ;
    class muff {
         friend void cuff::snip(muff &);
         ...
    } ;
    
  3. class muff {
          friend void cuff::snip(muff &);
         ...
    } ;
    class cuff {
    public:
          void snip(muff &) {  ... }
          ...
    } ;
    
2:

You've seen how to create mutual class friends. Can you create a more restricted form of friendship in which only some members of class B are friends to class A and some members of A are friends to B? Explain.

3:

What problems might the following nested class declaration have?

class Ribs
{
private:
    class Sauce
    {
          int soy;
          int sugar;
   public:
          Sauce(int s1, int s2) : soy(s1), sugar(s2) {  }
   } ;
   ...
} ;
4:

How does throw differ from return?

5:

Suppose you have a hierarchy of exception classes derived from a base exception class. In what order should you place catch blocks?

6:

Consider the Grand, Superb, and Magnificent classes defined in this chapter. Suppose pg is a type Grand * pointer assigned the address of an object of one of these three classes and that ps is a type Superb * pointer. What is the difference in how the following two code samples behave?

if (ps = dynamic_cast<Superb *>(pg))
    ps->say();  // sample #1
if (typeid(*pg) == typeid(Superb))
    (Superb *) pg)->say();  // sample #2
7:

How is the static_cast operator different from the dynamic_cast operator?

Programming Exercises

1:

Modify the Tv and Remote classes as follows:

  1. Make them mutual friends.

  2. Add a state variable member to the Remote class that describes whether the remote control is in normal or interactive mode.

  3. Add a Remote method that displays the mode.

  4. Provide the Tv class with a method for toggling the new Remote member. This method should work only if the Tv is in the on state.

Write a short program testing these new features.

2:

Modify Listing 15.10 so that hmean() throws an exception of type hmeanexcp and gmean() throws an exception of type gmeanexcp. Both of these exception types are to be classes derived from the exception class provided by the <exception> header file, and you should overload the what() method for each new class so that the message displayed by what() reports the function name and the nature of the problem. Also, upon catching an hmeanexcp exception, the program should prompt for a new data pair and continue with the loop, while upon catching a gmeanexcp exception, the program should break out of the loop and continue with the code following the loop.

CONTENTS