Standard exceptions

The set of exceptions used with the Standard C++ library is also available for your use. Generally it?s easier and faster to start with a standard exception class than to try to define your own. If the standard class doesn?t do exactly what you need, you can derive from it.
All standard exception classes derive ultimately from the class exception, defined in the header <exception>. The two main derived classes are logic_error and runtime_error, which are found in <stdexcept> (which itself includes <exception>). The class logic_error represents errors in programming logic, such as passing an invalid argument. Runtime errors are those that occur as the result of unforeseen forces such as hardware failure or memory exhaustion. Both runtime_error and logic_error provide a constructor that takes a std::string argument so that you can store a message in the exception object and extract it later with exception::what( ), as the following program illustrates.

Although the runtime_error constructor passes the message up to its std::exception subobject to hold, std::exception does not provide a constructor that takes a std::string argument. Therefore, you usually want to derive your exception classes from either runtime_error or logic_error (or one of their derivatives), and not from std::exception.

exception The base class for all the exceptions thrown by the C++ standard library. You can ask what( ) and retrieve the optional string with which the exception was initialized.
logic_error Derived from exception. Reports program logic errors, which could presumably be detected by inspection.
runtime_error Derived from exception. Reports runtime errors, which can presumably be detected only when the program executes.

Exception specifications

You?re not required to inform the people using your function what exceptions you might throw. Failure to do so can be considered uncivilized, however, because it means that users cannot be sure what code to write to catch all potential exceptions. Of course, if they have your source code, they can hunt through and look for throw statements, but often a library doesn?t come with sources. Good documentation can help alleviate this problem, but how many software projects are well documented? C++ provides syntax that allows you to tell the user what exceptions this function throws, so the user can handle them. This is the optional exception specification, which adorns a function?s declaration, appearing after the argument list.
The exception specification reuses the keyword throw, followed by a parenthesized list of all the types of potential exceptions that the function can throw. Your function declaration might look like this:

void f() throw(toobig, toosmall, divzero);

As far as exceptions are concerned, the traditional function declaration
void f();

means that any type of exception can be thrown from the function. If you say
void f() throw();

no exceptions whatsoever will be thrown from the function (so you?d better be sure that no functions farther down in the call chain let any exceptions propagate up!).

For good coding policy, good documentation, and ease-of-use for the function caller, always consider using exception specifications when you write functions that throw exceptions.

The unexpected( ) function

If your exception specification claims you?re going to throw a certain set of exceptions and then you throw something that isn?t in that set, what?s the penalty? The special function unexpected( ) is called when you throw something other than what appears in the exception specification. Should this unfortunate situation occur, the default implementation of unexpected calls the terminate( ) function mentioned earlier in this chapter.

The set_unexpected( ) function

Like terminate( ), the unexpected( ) mechanism allows you to install your own function to respond to unexpected exceptions. You do so with a function called set_unexpected( ), which, like set_terminate( ), takes the address of a function with no arguments and void return value. Also, because it returns the previous value of the unexpected( ) pointer, you can save it and restore it later. To use set_unexpected( ), you must include the header file <exception>. Here?s an example that shows a simple use of the features discussed so far in this section:

The classes Up and Fit are created solely to throw as exceptions. Often exception classes will be small, but they can certainly hold additional information so that the handlers can query for it.

The f( ) function promises in its exception specification to throw only exceptions of type Up and Fit, and from looking at the function definition, this seems plausible. Version one of g( ), called by f( ), doesn?t throw any exceptions, so this is true. But if someone changes g( ) so that it throws a different type of exception (like the second version in this example, which throws an int), the exception specification for f( ) is violated.

The my_unexpected( ) function has no arguments or return value, following the proper form for a custom unexpected( ) function. It simply displays a message so that you can see that it has been called and then exits the program (exit(0) is used here so that the book?s make process is not aborted). Your new unexpected( ) function should not have a return statement.

In main( ), the try block is within a for loop, so all the possibilities are exercised. In this way, you can achieve something like resumption. Nest the try block inside a for, while, do, or if and cause any exceptions to attempt to repair the problem; then attempt the try block again.

Only the Up and Fit exceptions are caught because those are the only exceptions that the programmer of f( ) said would be thrown. Version two of g( ) causes my_unexpected( ) to be called because f( ) then throws an int.

In the call to set_unexpected( ), the return value is ignored, but it can also be saved in a pointer to function and be restored later, as we did in the set_terminate( ) example earlier in this chapter.

A typical unexpected handler logs the error and terminates the program by calling exit( ). It can, however, throw another exception (or re-throw the same exception) or call abort( ). If it throws an exception of a type allowed by the function whose specification was originally violated, the search resumes at the call of the function with this exception specification. (This behavior is unique to unexpected( ).)
If the exception thrown from your unexpected handler is not allowed by the original function?s specification, one of the following occurs:

  1. If std::bad_exception (defined in <exception>) was in the function?s exception specification, the exception thrown from the unexpected handler is replaced with a std::bad_exception object, and the search resumes from the function as before.
  2. If the original function?s specification did not include std::bad_exception, terminate( ) is called.

The following program illustrates this behavior.

The my_uhandler1( ) handler throws an acceptable exception (A), so execution resumes at the first catch, which succeeds. The my_uhandler2( ) handler does not throw a valid exception (B), but since g specifies bad_exception, the B exception is replaced by a bad_exception object, and the second catch also succeeds. Since f does not include bad_exception in its specification, my_thandler( ) is called as a terminate handler. Thus, the output from this program is as follows:

caught an A from f
caught a bad_exception from g
terminate called

Typical uses of exceptions

Do use exceptions to do the following:

  1. Fix the problem and call the function which caused the exception again.
  2. Patch things up and continue without retrying the function.
  3. Do whatever you can in the current context and rethrow the same exception to a higher context.
  4. Do whatever you can in the current context and throw a different exception to a higher context.
  5. Terminate the program.
  6. Wrap functions (especially C library functions) that use ordinary error schemes so they produce exceptions instead.
  7. Simplify. If your exception scheme makes things more complicated, it is painful and annoying to use.
  8. Make your library and program safer. This is a short-term investment (for debugging) and a long-term investment (for application robustness).

When to use exception specifications
The exception specification is like a function prototype: it tells the user to write exception-handling code and what exceptions to handle. It tells the compiler the exceptions that might come out of this function so that it can detect violations at runtime.

Of course, you can?t always look at the code and anticipate which exceptions will arise from a particular function. Sometimes, the functions it calls produce an unexpected exception, and sometimes an old function that didn?t throw an exception is replaced with a new one that does, and you get a call to unexpected( ). Any time you use exception specifications or call functions that do, consider creating your own unexpected( ) function that logs a message and then either throws an exception or aborts the program.

As we explained earlier, you should avoid using exception specifications in template classes, since you can?t anticipate what types of exceptions the template parameter classes might throw.

Start with standard exceptions
Check out the Standard C++ library exceptions before creating your own. If a standard exception does what you need, chances are it?s a lot easier for your user to understand and handle.
If the exception type you want isn?t part of the standard library, try to derive one from an existing standard exception. It?s nice if your users can always write their code to expect the what( ) function defined in the exception( ) class interface.
Nest your own exceptions
If you create exceptions for your particular class, it?s a good idea to nest the exception classes either inside your class or inside a namespace containing your class, to provide a clear message to the reader that this exception is used only for your class. In addition, it prevents the pollution of the global namespace.
You can nest your exceptions even if you?re deriving them from C++ standard exceptions.
Use exception hierarchies
Using exception hierarchies is a valuable way to classify the types of critical errors that might be encountered with your class or library. This gives helpful information to users, assists them in organizing their code, and gives them the option of ignoring all the specific types of exceptions and just catching the base-class type. Also, any exceptions added later by inheriting from the same base class will not force all existing code to be rewritten?the base-class handler will catch the new exception.
Of course, the Standard C++ exceptions are a good example of an exception hierarchy and one on which you can build.
Multiple inheritance (MI)
As you?ll read in Chapter 9, the only essential place for MI is if you need to upcast an object pointer to two different base classes?that is, if you need polymorphic behavior with both of those base classes. It turns out that exception hierarchies are useful places for multiple inheritance because a base-class handler from any of the roots of the multiply inherited exception class can handle the exception.
Catch by reference, not by value
We explained in the section ?Exception matching? earlier that you should catch exceptions by reference for two reasons:

  • To avoid making a needless copy of the exception object when it is passed to the handler,
  • To avoid object slicing when catching a derived exception as a base class object

Although you can also throw and catch pointers, by doing so you introduce more coupling?the thrower and the catcher must agree on how the exception object is allocated and cleaned up. This is a problem because the exception itself might have occurred from heap exhaustion. If you throw exception objects, the exception-handling system takes care of all storage.
Throw exceptions in constructors
Because a constructor has no return value, you?ve previously had two ways to report an error during construction:

  • Set a nonlocal flag and hope the user checks it.
  • Return an incompletely created object and hope the user checks it.

This problem is serious because C programmers have come to rely on an implied guarantee that object creation is always successful, which is not unreasonable in C in which types are so primitive. But continuing execution after construction fails in a C++ program is a guaranteed disaster, so constructors are one of the most important places to throw exceptions?now you have a safe, effective way to handle constructor errors. However, you must also pay attention to pointers inside objects and the way cleanup occurs when an exception is thrown inside a constructor.
Don?t cause exceptions in destructors
Because destructors are called in the process of throwing other exceptions, you?ll never want to throw an exception in a destructor or cause another exception to be thrown by some action you perform in the destructor. If this happens, a new exception can be thrown before the catch-clause for an existing exception is reached, which will cause a call to terminate( ).
If you call any functions inside a destructor that can throw exceptions, those calls should be within a try block in the destructor, and the destructor must handle all exceptions itself. None must escape from the destructor.
Avoid naked pointers
See Wrapped.cpp earlier in this chapter. A naked pointer usually means vulnerability in the constructor if resources are allocated for that pointer. A pointer doesn?t have a destructor, so those resources aren?t released if an exception is thrown in the constructor. Use auto_ptr for pointers that reference heap memory.

Share
Tweet
Share
Pin