EXPLORATION 45

image

Exceptions

You may have been dismayed by the lack of error checking and error handling in the Explorations so far. That’s about to change. C++, like most modern programming languages, supports exceptions as a way to jump out of the normal flow of control in response to an error or other exceptional condition. This Exploration introduces exceptions: how to throw them, how to catch them, when the language and library use them, and when and how you should use them.

Introducing Exceptions

Exploration 9 introduced vector’s at member function, which retrieves a vector element at a particular index. At the time, I wrote that most programs you read would use square brackets instead. Now is a good time to examine the difference between square brackets and the at function. First, take a look at two programs. Listing 45-1 shows a simple program that uses a vector.

Listing 45-1.  Accessing an Element of a Vector

#include <iostream>
#include <vector>
 
int main()
{
  std::vector<int> data{ 10, 20 };
  data.at(5) = 0;
  std::cout << data.at(5) << ' ';
}

What do you expect to happen when you run this program?

_____________________________________________________________

Try it. What actually happens?

_____________________________________________________________

The vector index, 5, is out of bounds. The only valid indices for data are 0 and 1, so it’s no wonder that the program terminates with a nastygram. Now consider the program in Listing 45-2.

Listing 45-2.  A Bad Way to Access an Element of a Vector

#include <iostream>
#include <vector>
 
int main()
{
  std::vector<int> data{ 10, 20 };
  data[5] = 0;
  std::cout << data[5] << ' ';
}

What do you expect to happen when you run this program?

_____________________________________________________________

Try it. What actually happens?

_____________________________________________________________

The vector index, 5, is still out of bounds. If you still receive a nastygram, you get a different one than before. On the other hand, the program might run to completion without indicating any error. You might find that disturbing, but such is the case of undefined behavior. Anything can happen.

That, in a nutshell, is the difference between using subscripts ([]) and the at member function. If the index is invalid, the at member function causes the program to terminate in a predictable, controlled fashion. You can write additional code and avoid termination, take appropriate actions to clean up prior to termination, or let the program end.

The subscript operator, on the other hand, results in undefined behavior if the index is invalid. Anything can happen, so you have no control—none whatsoever. If the software is controlling, say, an airplane, then “anything” involves many options that are too unpleasant to imagine. On a typical desktop workstation, a more likely scenario is that the program crashes, which is a good thing, because it tells you that something went wrong. The worst possible consequence is that nothing obvious happens, and the program silently uses a garbage value and keeps running.

The at member function, and many other functions, can throw exceptionsto signal an error. When a program throws an exception, the normal, statement-by-statement progression of the program is interrupted. Instead, a special exception-handling system takes control of the program. The standard gives some leeway in how this system actually works, but you can imagine that it forces functions to end and destroys local objects and parameters, although the functions do not return a value to the caller. Instead, functions are forcefully ended, one at a time, and a special code block catches the exception. Use the try-catch statement to set up these special code blocks in a program. A catch block is also called an exception handler. Normal code execution resumes after the handler finishes its work:

try {
  throw std::runtime_error("oops");
} catch (std::runtime_error const& ex) {
  std::cerr << ex.what() << ' ';
}

When a program throws an exception (with the throw keyword), it throws a value, called an exception object, which can be an object of nearly any type. By convention, exception types, such as std::runtime_error, inherit from the std::exception class or one of several subclasses that the standard library provides. Third-party class libraries sometimes introduce their own exception base class.

An exception handler also has an object declaration, which has a type, and the handler accepts only exception objects of the same type or of a derived type. If no exception handler has a matching type, or if you don’t write any handler at all, the program terminates, as happens with Listing 45-1. The remainder of this Exploration examines each aspect of exception handling in detail.

Catching Exceptions

An exception handler is said to catch an exception. Write an exception handler at the end of a try: the try keyword is followed by a compound statement (it must be compound), followed by a series of handlers. Each handler starts with a catch keyword, followed by parentheses that enclose the declaration of an exception-handler object. After the parentheses is a compound statement that is the body of the exception handler.

When the type of the exception object matches the type of the exception-handler object, the handler is deemed a match, and the handler object is initialized with the exception object. The handler declaration is usually a reference, which avoids copying the exception object unnecessarily. Most handlers don’t have to modify the exception object, so the handler declaration is typically a reference to const. A “match” is when the exception object’s type is the same as the handler’s declared type or a class derived from the handler’s declared type, ignoring whether the handler is const or a reference.

The exception-handling system destroys all objects that it constructed in the try part of the statement prior to throwing the exception, then it transfers control to the handler, so the handler’s body runs normally, and control resumes with the statement after the end of the entire try-catch statement, that is, after the statement’s last catch handler. The handler types are tried in order, and the first match wins. Thus, you should always list the most specific types first and base class types later.

A base class exception handler type matches any exception object of a derived type. To handle all exceptions that the standard library might throw, write the handler to catch std::exception (declared in <exception>), which is the base class for all standard exceptions. Listing 45-3 demonstrates some of the exceptions that the std::string class can throw. Try out the program by typing strings of varying length.

Listing 45-3.  Forcing a string to Throw Exceptions

#include <cstdlib> 
#include <exception>
#include <iostream>
#include <stdexcept>
#include <string>
 
int main()
{
  std::string line{};
  while (std::getline(std::cin, line))
  {
    try
    {
      line.at(10) = ' ';                               // can throw out_of_range
      if (line.size() < 20)
         line.append(line.max_size(), '*'), // can throw length_error
      for (std::string::size_type size(line.size());
           size < line.max_size();
           size = size * 2)
      {
        line.resize(size);                             // can throw bad_alloc
      }
      line.resize(line.max_size());                    // can throw bad_alloc
      std::cout << "okay ";
    }
    catch (std::out_of_range const& ex)
    {
       std::cout << ex.what() << ' ';
       std::cout << "string index (10) out of range. ";
    }
    catch (std::length_error const& ex)
    {
      std::cout << ex.what() << ' ';
      std::cout << "maximum string length (" << line.max_size() << ") exceeded. ";
    }
    catch (std::exception const& ex)
    {
      std::cout << "other exception: " << ex.what() << ' ';
    }
    catch (...)
    {
      std::cout << "Unknown exception type. Program terminating. ";
      std::abort();
    }
  }
}

If you type a line that contains 10 or fewer characters, the line.at(10) expression throws a std::out_of_range exception. If the string has more than 10 characters, but fewer than 20, the program tries to append the maximum string size repetitions of an asterisk ('*'), which results in std::length_error. If the initial string is longer than20 characters, the program tries to increase the string size, using ever-growing sizes. Most likely, the size will eventually exceed available memory, in which case the resize() function will throw std::bad_alloc. If you have lots and lots of memory, the next error situation forces the string size to the limit that string supports and then tries to add another character to the string, which causes the push_back function to throw std::length_error. (The max_size member function returns the maximum number of elements that a container, such as std::string, can contain.)

The base class handler catches any exceptions that the first two handlers miss; in particular, it catches std::bad_alloc. The what() member function returns a string that describes the exception. The exact contents of the string vary by implementation. Any nontrivial application should define its own exception classes and hide the standard library exceptions from the user. In particular, the strings returned from what() are implementation-defined and are not necessarily helpful. Catching bad_alloc is especially tricky, because if the system is running out of memory, the application might not have enough memory to save its data prior to shutting down. You should always handle bad_alloc explicitly, but I wanted to demonstrate a handler for a base class.

The final catch handler uses an ellipsis (...) instead of a declaration. This is a catch-all handler that matches any exception. If you use it, it must be last, because it matches every exception object, of any type. Because the handler doesn’t know the type of the exception, it has no way to access the exception object. This catch-all handler prints a message and then calls std::abort() (declared in <exception>), which immediately ends the program. Because the std::exception handler catches all standard library exceptions, the final catch-all handler is not really needed, but I wanted to show you how it works.

Throwing Exceptions

A throw expression throws an exception. The expression consists of the throw keyword followed by an expression, namely, the exception object. The standard exceptions all take a string argument, which becomes the value returned from the what() member function.

throw std::out_of_range("index out of range");

The messages that the standard library uses for its own exceptions are implementation-defined, so you cannot rely on them to provide any useful information.

You can throw an exception anywhere an expression can be used, sort of. The type of a throw expression is void, which means it has no type. Type void is not allowed as an operand for any arithmetic, comparison, etc., operator. Thus, realistically, a throw expression is typically used in an expression statement, all by itself.

You can throw an exception inside a catch handler, which low-level code and libraries often do. You can throw the same exception object or a brand-new exception. To re-throw the same object, use the throw keyword without any expression.

catch (std::out_of_range const& ex)
{
  std::cout << "index out of range ";
  throw;
}

A common case for re-throwing an exception is inside a catch-all handler. The catch-all handler performs some important cleanup work and then propagates the exception so the program can handle it.

If you throw a new exception, the exception-handling system takes over normally. Control leaves the try-catch block immediately, so the same handler cannot catch the new exception.

Program Stack

To understand what happens when a program throws an exception, you must first understand the nature of the program stack, sometimes called the execution stack. Procedural and similar languages use a stack at runtime to keep track of function calls, function arguments, and local variables. The C++ stack also helps keep track of exception handlers.

When a program calls a function, the program pushes a frame onto the stack. The frame has information such as the instruction pointer and other registers, arguments to the function, and possibly some memory for the function’s return value. When a function starts, it might set aside some memory on the stack for local variables. Each local scope pushes a new frame onto the stack. (The compiler might be able to optimize away a physical frame for some local scopes, or even an entire function. Conceptually, however, the following applies.)

While a function executes, it typically constructs a variety of objects: function arguments, local variables, temporary objects, and so on. The compiler keeps track of all the objects the function must create, so it can properly destroy them when the function returns. Local objects are destroyed in the opposite order of their creation.

Frames are dynamic, that is, they represent function calls and the flow of control in a program, not the static representation of source code. Thus, a function can call itself, and each call results in a new frame on the stack, and each frame has its own copy of all the function arguments and local variables.

When a program throws an exception, the normal flow of control stops, and the C++ exception-handling mechanism takes over. The exception object is copied to a safe place, off the execution stack. The exception-handling code looks through the stack for a try statement. When it finds a try statement, it checks the types for each handler in turn, looking for a match. If it doesn’t find a match, it looks for the next try statement, farther back in the stack. It keeps looking until it finds a matching handler or it runs out of frames to search.

When it finds a match, it pops frames off the execution stack, calling destructors for all local objects in each popped frame, and continues to pop frames until it reaches the handler. Popping frames from the stack is also called unwinding the stack.

After unwinding the stack, the exception object initializes the handler’s exception object, and then the catch body is executed. After the catch body exits normally, the exception object is freed, and execution continues with the statement that follows the end of the last sibling catch block.

If the handler throws an exception, the search for a matching handler starts anew. A handler cannot handle an exception that it throws, nor can any of its sibling handlers in the same try statement.

If no handler matches the exception object’s type, the std::terminate function is called, which aborts the program. Some implementations will pop the stack and free local objects prior to calling terminate, but others won’t.

Listing 45-4 can help you visualize what is going on inside a program when it throws and catches an exception.

Listing 45-4.  Visualizing an Exception

 1 #include <exception>
 2 #include <iostream>
 3 #include <string>
 4
 5 /// Make visual the construction and destruction of objects.
 6 class visual
 7 {
 8 public:
 9   visual(std::string const& what)
10   : id_{serial_}, what_{what}
11   {
12     ++serial_;
13     print("");
14   }
15   visual(visual const& ex)
16   : id_{ex.id_}, what_{ex.what_}
17   {
18     print("copy ");
19   }
20   ~visual()
21   {
22     print("~");
23   }
24   void print(std::string const& label)
25   const
26   {
27     std::cout << label << "visual(" << what_ << ": " << id_ << ") ";
28   }
29 private:
30   static int serial_;
31   int const id_;
32   std::string const what_;
33 };
34
35 int visual::serial_{0};
36
37 void count_down(int n)
38 {
39   std::cout << "start count_down(" << n << ") ";
40   visual v{"count_down local"};
41   try
42   {
43     if (n == 3)
44       throw visual("exception");
45     else if (n > 0)
46       count_down(n - 1);
47   }
48   catch (visual ex)
49   {
50     ex.print("catch ");
51     throw;
52   }
53   std::cout << "end count_down(" << n << ") ";
54 }
55
56 int main()
57 {
58   try
59   {
60     count_down(2);
61     count_down(4);
62   }
63   catch (visual const ex)
64   {
65     ex.print("catch ");
66   }
67   std::cout << "All done! ";
68 }

The visual class helps show when and how objects are constructed, copied, and destroyed. The count_down function throws an exception when its argument equals 3, and it calls itself when its argument is positive. The recursion stops for non-positive arguments. To help you see function calls, it prints the argument upon entry to, and exit from, the function.

The first call to count_down does not trigger the exception, so you should see normal creation and destruction of the local visual object. Write exactly what the program should print as a result of line 60 (count_down(2)).

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

The next call to count_down from main (line 61) allows count_down to recurse once before throwing an exception. So count_down(4) calls count_down(3). The local object, v, is constructed inside the frame for  count_down(4), and a new instance of v is constructed inside the frame for count_down(3). Then the exception object is created and thrown. (See Figure 45-1.)

9781430261933_Fig45-01.jpg

Figure 45-1. Program stack when the exception is thrown

The exception is caught inside count_down, so its frame is not popped. The exception object is then copied to ex (line 48), and the exception handler begins. It prints a message and then re-throws the original exception object (line 51). The exception-handling mechanism treats this exception the same way it treats any other: the try statement’s frame is popped, and then the count_down function’s frame is popped. Local objects are destroyed (including ex and v). The final statement in count_down does not execute.

The stack is unwound, and the try statement inside the call to count_down(4) is found, and once again, the exception object is copied to a new instance of ex. (See Figure 45-2.) The exception handler prints a message and re-throws the original exception. The count_down(4) frame is popped, returning control to the try statement in main. Again, the final statement in count_down does not execute.

9781430261933_Fig45-02.jpg

Figure 45-2. Program stack after re-throwing exception

The exception handler in main gets its turn, and this handler prints the exception object one last time (line 63). After the handler prints a message, and the catch body reaches its end, the local exception object and the original exception object are destroyed. Execution then continues normally on line 67. The final output is

start count_down(2)
visual(count_down local: 0)
start count_down(1)
visual(count_down local: 1)
start count_down(0)
visual(count_down local: 2)
end count_down(0)
~visual(count_down local: 2)
end count_down(1)
~visual(count_down local: 1)
end count_down(2)
~visual(count_down local: 0)
start count_down(4)
visual(count_down local: 3)
start count_down(3)
visual(count_down local: 4)
visual(exception: 5)
copy visual(exception: 5)
catch visual(exception: 5)
~visual(exception: 5)
~visual(count_down local: 4)
copy visual(exception: 5)
catch visual(exception: 5)
~visual(exception: 5)
~visual(count_down local: 3)
copy visual(exception: 5)
catch visual(exception: 5)
~visual(exception: 5)
~visual(exception: 5)
All done!

Standard Exceptions

The standard library defines several standard exception types. The base class, exception, is declared in the <exception> header. Most of the other exception classes are defined in the <stdexcept> header. If you want to define your own exception class, I recommend deriving it from one of the standard exceptions in <stdexcept>.

The standard exceptions are divided into two categories (with two base classes that derive directly from exception).

  • Runtime errors (std::runtime_error) are exceptions that you cannot detect or prevent merely by examining the source code. They arise from conditions that you can anticipate, but not prevent.
  • Logic errors (std::logic_error) are the result of programmer error. They represent violations of preconditions, invalid function arguments, and other errors that the programmer should prevent in code.

The other standard exception classes in <stdexcept> derive from these two. Most standard library exceptions are logic errors. For example, out_of_range inherits from logic_error. The at member function (and others) throws out_of_range when the index is out of range. After all, you should check indices and sizes, to be sure your vector and string usage are correct, and not rely on exceptions. The exceptions are there to provide clean, orderly shutdown of your program when you do make a mistake (and we all make mistakes).

Your library reference tells you which functions throw which exceptions, such as at can throw out_of_range. Many functions might throw other, undocumented exceptions too, depending on the library’s and compiler’s implementation. In general, however, the standard library uses few exceptions. Instead, most of the library yields undefined behavior when you provide bad input. The I/O streams do not ordinarily throw any exceptions, but you can arrange for them to throw exceptions when bad errors happen, as I explain in the next section.

I/O Exceptions

You learned about I/O stream state bits in Exploration 31. State bits are important, but checking them repeatedly is cumbersome. In particular, many programs fail to check the state bits of output streams, especially when writing to the standard output. That’s just plain, old-fashioned laziness. Fortunately, C++ offers an avenue for programmers to gain I/O safety without much extra work: the stream can throw an exception when I/O fails.

In addition to state bits, each stream also has an exception mask. The exception mask tells the stream to throw an exception if a corresponding state bit changes value. For example, you could set badbit in the exception mask and never write an explicit check for this unlikely occurrence. If a serious I/O error were to occur, causing badbit to become set, the stream would throw an exception. You can write a handler at a high level to catch the exception and terminate the program cleanly, as shown in Listing 45-5.

Listing 45-5.  Using an I/O Stream Exception Mask

#include <iostream>
 
int main()
{
  std::cin.exceptions(std::ios_base::badbit);
  std::cout.exceptions(std::ios_base::badbit);
 
  int x{};
  try
  {
    while (std::cin >> x)
      std::cout << x << ' ';
    if (not std::cin.eof()) // failure without eof means invalid input
      std::cerr << "Invalid integer input. Program terminated. ";
  }
  catch(std::ios_base::failure const& ex)
  {
    std::cerr << "Major I/O failure! Program terminated. ";
    std::terminate();
  }
}

As you can see, the exception class is named std::ios_base::failure. Also note a new output stream: std::cerr. The <iostream> header actually declares several standard I/O streams. So far, I’ve used only cin and cout, because that’s all we’ve needed. The cerr stream is an output stream dedicated to error output. In this case, separating normal output (to cout) from error output (to cerr) is important, because cout might have a fatal error (say, a disk is full), so any attempt to write an error message to cout would be futile. Instead, the program writes the message to cerr. There’s no guarantee that writing to cerr would work, but at least there’s a chance; for example, the user might redirect the standard output to a file (and thereby risk encountering a disk-full error), while allowing the error output to appear on a console.

Recall that when an input stream reaches the end of the input, it sets eofbit in its state mask. Although you can also set this bit in the exceptions mask, I can’t see any reason why you would want to. If an input operation does not read anything useful from the stream, the stream sets failbit. The most common reasons that the stream might not read anything is end of file (eofbit is set) or an input formatting error (e.g., text in the input stream when the program tries to read a number). Again, it’s possible to set failbit in the exception mask, but most programs rely on ordinary program logic to test the state of an input stream. Exceptions are for exceptional conditions, and end-of-file is a normal occurrence when reading from a stream.

The loop ends when failbit is set, but you have to test further to discover whether failbit is set, because of a normal end-of-file condition or because of malformed input. If eofbit is also set, you know the stream is at its end. Otherwise, failbit must be owing to malformed input.

As you can see, exceptions are not the solution for every error situation. Thus, badbit is the only bit in the exception mask that makes sense for most programs, especially for input streams. An output stream sets failbit if it cannot write the entire value to the stream. Usually, such a failure occurs because of an I/O error that sets badbit, but it’s at least theoretically possible for output failure to set failbit without also setting badbit. In most situations, any output failure is cause for alarm, so you might want to throw an exception for failbit with output streams and badbit with input streams.

std::cin.exceptions(std::ios_base::badbit);
std::cout.exceptions(std::ios_base::failbit);

Custom Exceptions

Exceptions simplify coding by removing exceptional conditions from the main flow of control. You can and should use exceptions for many error situations. For example, the rational class (most recently appearing in Exploration 40) has, so far, completely avoided the issue of division by zero. A better solution than invoking undefined behavior (which is what happens when you divide by zero) is to throw an exception anytime the denominator is zero. Define your own exception class by deriving from one of the standard exception base classes, as shown in Listing 45-6. By defining your own exception class, any user of rational can easily distinguish its exceptions from other exceptions.

Listing 45-6.  Throwing an Exception for a Zero Denominator

#ifndef RATIONAL_HPP_
#define RATIONAL_HPP_
 
#include <stdexcept>
#include <string>
 
class rational
{
public:
  class zero_denominator : public std::logic_error
  {
  public:
    zero_denominator(std::string const& what_arg) : logic_error{what_arg} {}
  };
 
  rational() : rational{0} {}
  rational(int num) : numerator_{num}, denominator_{1} {}
  rational(int num, int den) : numerator_{num}, denominator_{den}
  {
    if (denominator_ == 0)
      throw zero_denominator{"zero denominator"};
    reduce();
  }
... omitted for brevity ...
};
#endif

Notice how the zero_denominator class nests within the rational class. The nested class is a perfectly ordinary class. It has no connection with the outer class (as with a Java inner class), except the name. The nested class gets no special access to private members in the outer class, nor does the outer class get special access to the nested class name. The usual rules for access levels determine the accessibility of the nested class. Some nested classes are private helper classes, so you would declare them in a private section of the outer class definition. In this case, zero_denominator must be public, so that callers can use the class in exception handlers.

To use a nested class name outside the outer class, you must use the outer class and the nested class names, separated by a scope operator (::). The nested class name has no significance outside of the outer class’s scope. Thus, nested classes help avoid name collisions. They also provide clear documentation for the human reader who sees the type in an exception handler:

catch (rational::zero_denominator const& ex) {
  std::cerr << "zero denominator in rational number ";
}

Find all other places in the rational class that have to check for a zero denominator and add appropriate error-checking code to throw zero_denominator.

All roads lead to reduce(), so one approach is to replace the assertion with a check for a zero denominator, and throw the exception there. You don’t have to modify any other functions, and even the extra check in the constructor (illustrated in Listing 45-6) is unnecessary. Listing 45-7 shows the latest implementation of reduce().

Listing 45-7.  Checking for a Zero Denominator in reduce()

void rational::reduce()
{
  if (denominator_ == 0)
    throw zero_denominator{"denominator is zero"};
  if (denominator_ < 0)
  {
    denominator_ = -denominator_;
    numerator_ = -numerator_;
  }
  int div{gcd(numerator_, denominator_)};
  numerator_ = numerator_ / div;
  denominator_ = denominator_ / div;
}

Don’t Throw Exceptions

Certain functions should never throw an exception. For example, the numerator() and denominator() functions simply return an integer. There is no way they can throw an exception. If the compiler knows that the functions never throw an exception, it can generate more efficient object code. With these specific functions, the compiler probably expands the functions inline to access the data members directly, so in theory, it doesn’t matter. But maybe you decide not to make the functions inline (for any of the reasons listed in Exploration 30). You still want to be able to tell the compiler that the functions cannot throw any exceptions. Enter the noexcept qualifier.

To tell the compiler that a function does not throw an exception, add the noexcept qualifier after the function parameters (after const but before override).

int numerator() const noexcept;

What happens if you break the contact? Try it. Write a program that calls a trivial function that is qualified as noexcept, but throws an exception. Try to catch the exception in main(). What happens?

_____________________________________________________________

If your program looks anything like mine in Listing 45-8, the catch is supposed to catch the exception, but it doesn’t. The compiler trusted noexcept and did not generate the normal exception-handling code. As a result, when function() throws an exception, the only thing the program can do is terminate immediately.

Listing 45-8.  Throwing an Exception from a noexcept Function

#include <iostream>
#include <exception>
 
void function() noexcept
{
  throw std::exception{};
}
 
int main()
{
  try {
    function();
  } catch (std::exception const& ex) {
    std::cout << "Gotcha! ";
  }
}

So you must use noexcept judiciously. If function a() calls only functions that are marked noexcept, the author of a() might decide to make a() noexcept too. But if one of those functions, say, b(), changes and is no longer noexcept, then a() is in trouble. If b() throws an exception, the program unceremoniously terminates. So use noexcept only if you can guarantee that the function cannot throw an exception now and will never change in the future to throw an exception. So it is probably safe for numerator() and denominator() to be noexcept in the rational class, as well as the default and single-argument constructors, but I can’t think of any other member function that can be noexcept.

Exceptional Advice

The basic mechanics of exceptions are easy to grasp, but their proper use is more difficult. The applications programmer has three distinct tasks: catching exceptions, throwing exceptions, and avoiding exceptions.

You should write your programs to catch all exceptions, even the unexpected ones. One approach is for your main program to have a master try statement around the entire program body. Within the program, you might use targeted try statements to catch specific exceptions. The closer you are to the source of the exception, the more contextual information you have, and the better you can ameliorate the problem, or at least present the user with more useful messages.

This outermost try statement catches any exceptions that other statements miss. It is a last-ditch attempt to present a coherent and helpful error message before the program terminates abruptly. At a minimum, tell the user that the program is terminating because of an unexpected exception.

In an event-driven program, such as a GUI application, exceptions are more problematic. The outermost try statement shuts down the program, closing all windows. Most event handlers should have their own try statement to handle exceptions for that particular menu pick, keystroke event, and so on.

Within the body of your program, better than catching exceptions is avoiding them. Use the at member function to access elements of a vector, but you should write the code so you are confident that the index is always valid. Index and length exceptions are signs of programmer error.

When writing low-level code, throw exceptions for most error situations that should not happen or that otherwise reflect programmer error. Some error conditions are especially dangerous. For example, in the rational class, a denominator should never be zero or negative after reduce() returns. If a condition arises when the denominator is indeed zero or negative, the internal state of the program is corrupt. If the program were to attempt a graceful shutdown, saving all files, etc., it might end up writing bad data to the files. Better to terminate immediately and rely on the most recent backup copy, which your program made while its state was still known to be good. Use assertions, not exceptions, for such emergencies.

Ideally, your code should validate user input, check vector indices, and make sure all arguments to all functions are valid before calling the functions. If anything is invalid, your program can tell the user with a clear, direct message and avoid exceptions entirely. Exceptions are a safety net when your checks fail or you forget to check for certain conditions.

As a general rule, libraries should throw exceptions, not catch them. Applications tend to catch exceptions more than throw them. As programs grow more complex, I will highlight situations that call for exceptions, throwing or catching.

Now that you know how to write classes, overload operators, and handle errors, you need only learn about some additional operators before you can start implementing fully functional classes of your own. The next Exploration revisits some familiar operators and introduces a few new ones.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset