Friday 16 November 2012

Exception Handling in C++

Unit 5 Exception Handling
Structure
5.1 Introduction
5.2 Throwing an exception
Self Assessment Questions
5.3 Catching an exception
5.3.1 The try block
5.3.2 Exception handlers
Self Assessment Questions
5.4 Termination vs. Resumption
5.5 Exception specifications
5.5.1 unexpected()
5.5.2 set_unexpected()
5.5.3 Catching any exception
Self Assessment Questions
5.6 Rethrowing an exception
5.7 Uncaught exceptions
5.7.1 terminate()
5.7.2 set_terminate()
Self Assessment Questions
5.8 Standard exceptions
5.9 Programming with exceptions
5.9.1 When to avoid exception
5.9.2 Using exceptions
5.10 Summary
5.11 Terminal Questions
References and Further Reading
5.1 Introduction
Improved error recovery is one of the most powerful ways you can increase the robustness of your code. Unfortunately, it’s almost accepted practice to ignore error conditions, as if we’re in a state of denial about errors. Some of the reason is no doubt the tediousness and code bloat of checking for many errors. For example, printf() returns the number of characters that were successfully printed, but virtually no one checks this value. The proliferation of code alone would be disgusting, not to mention the difficulty it would add in reading the code. The problem with C’s approach to error handling could be thought of as one of coupling – the user of a function must tie the error-handling code so closely to that function that it becomes too ungainly and awkward to use.
One of the major features in C++ is exception handling, which is a better way of thinking about and handling errors. With exception handling:
· Error-handling code is not nearly so tedious to write, and it doesn't become mixed up with your "normal" code. You write the code you want to happen; later in a separate section you write the code to cope with the problems. If you make multiple calls to a function, you handle the errors from that function once, in one place.
· Errors cannot be ignored. If a function needs to send an error message to the caller of that function, it “throws” an object representing that error out of the function. If the caller doesn’t “catch” the error and handle it, it goes to the next enclosing scope, and so on until someone catches the error.
Objectives
Understanding the details provided in this unit will enable you to:
· Describe exception, exception handling, and the necessity to have exception handling.
· Understand the usage of the try and catch blocks.
· Amass insights into the models of exception-handling, and the necessity of exception specifications.
· Understand the special functions unexpected(), terminate(), and more.
· Understand how to deal with uncaught exceptions, and how to rethrow exceptions.
· Describe standard exceptions.
· Understand the dos and don’ts of exception handling.
· Write advanced C++ programs that are self sufficient in handling all types of exceptions themselves.
5.2 Throwing an Exception
If you encounter an exceptional situation in your code – that is, one where you don’t have enough information in the current context to decide what to do – you can send information about the error into a larger context by creating an object containing that information and “throwing” it out of your current context. This is called throwing an exception. Here’s what it looks like:
throw myerror(“something bad happened”);
myerror is an ordinary class, which takes a char* as its argument. You can use any type when you throw (including built-in types), but often you’ll use special types created just for throwing exceptions. The keyword throw causes a number of relatively magical things to happen. First it creates an object that isn’t there under normal program execution, and of course the constructor is called for that object. Then the object is, in effect, “returned” from the function, even though that object type isn’t normally what the function is designed to return.
A simplistic way to think about exception handling is as an alternate return mechanism, although you get into trouble if you take the analogy too far – you can also exit from ordinary scopes by throwing an exception. But a value is returned, and the function or scope exits. Any similarity to function returns ends there because where you return to is someplace completely different than for a normal function call. (You end up in an appropriate exception handler that may be miles away from where the exception was thrown.) In addition, only objects that were successfully created at the time of the exception are destroyed (unlike a normal function return that assumes all the objects in the scope must be destroyed). Of course, the exception object itself is also properly cleaned up at the appropriate point. In addition, you can throw as many different types of objects as you want. Typically, you’ll throw a different type for each different type of error. The idea is to store the information in the object and the type of object, so someone in the bigger context can figure out what to do with your exception.
Self Assessment Questions
1. Describe exception handling.
2. What is an exception handler?
5.3 Catching an Exception
If a function throws an exception, it must assume that exception is caught and dealt with. As mentioned before, one of the advantages of C++ exception handling is that it allows you to concentrate on the problem you’re actually trying to solve in one place, and then deal with the errors from that code in another place.
5.3.1 The try block
If you’re inside a function and you throw an exception (or a called function throws an exception), that function will exit in the process of throwing. If you don’t want a throw to leave a function, you can set up a special block within the function where you try to solve your actual programming problem (and potentially generate exceptions). This is called the try block because you try your various function calls there. The try block is an ordinary scope, preceded by the keyword try:
try {
// Code that may generate exceptions
}
If you were carefully checking for errors without using exception handling, you’d have to surround every function call with setup and test code, even if you call the same function several times. With exception handling, you put everything in a try block without error checking. This means your code is a lot easier to write and easier to read because the goal of the code is not confused with the error checking.
5.3.2 Exception handlers
Of course, the thrown exception must end up someplace. This is the exception handler, and there’s one for every exception type you want to catch. Exception handlers immediately follow the try block and are denoted by the keyword catch:
try {
// code that may generate exceptions
} catch(type1 id1) {
// handle exceptions of type1
} catch(type2 id2) {
// handle exceptions of type2
}
// etc...
Each catch clause (exception handler) is like a little function that takes a single argument of one particular type. The identifier (id1, id2, and so on) may be used inside the handler, just like a function argument, although sometimes there is no identifier because it’s not needed in the handler – the exception type gives you enough information to deal with it.
The handlers must appear directly after the try block. If an exception is thrown, the exception handling mechanism goes hunting for the first handler with an argument that matches the type of the exception. Then it enters that catch clause, and the exception is considered handled. (The search for handlers stops once the catch clause is finished.) Only the matching catch clause executes; it’s not like a switch statement where you need a break after each case to prevent the remaining ones from executing. Notice that, within the try block, a number of different function calls might generate the same exception, but you only need one handler.
Self Assessment Questions
1. What is a try block?
2. How exceptions that are thrown are handled?
5.4 Termination vs. Resumption
There are two basic models in exception-handling theory. In termination (which is what C++ supports) you assume the error is so critical there’s no way to get back to where the exception occurred. Whoever threw the exception decided there was no way to salvage the situation, and they don’t want to come back. The alternative is called resumption. It means the exception handler is expected to do something to rectify the situation, and then the faulting function is retried, presuming success the second time. If you want resumption, you still hope to continue execution after the exception is handled, so your exception is more like a function call – which is how you should set up situations in C++ where you want resumption-like behavior (that is, don’t throw an exception; call a function that fixes the problem). Alternatively, place your try block inside a while loop that keeps reentering the try block until the result is satisfactory.
Historically, programmers using operating systems that supported resumptive exception handling eventually ended up using termination-like code and skipping resumption. So although resumption sounds attractive at first, it seems it isn’t quite so useful in practice. One reason may be the distance that can occur between the exception and its handler; it’s one thing to terminate to a handler that’s far away, but to jump to that handler and then back again may be too conceptually difficult for large systems where the exception can be generated from many points.
5.5 Exception Specifications
You’re not required to inform the person using your function what exceptions you might throw. However, this is considered uncivilized because it means he cannot be sure what code to write to catch all potential exceptions. Of course, if he has your source code, he can hunt through and look for throw statements, but very often a library doesn’t come with sources. C++ provides syntax to allow you to politely tell the user what exceptions this function throws, so the user may handle them. This is the exception specification and it’s part of the function declaration, appearing after the argument list.
The exception specification reuses the keyword throw, followed by a parenthesized list of all the potential exception types. So your function declaration may look like
void f() throw(toobig, toosmall, divzero);
With exceptions, the traditional function declaration
void f();
means that any type of exception may be thrown from the function. If you say
void f() throw();
it means that no exceptions are thrown from a function.
For good coding policy, good documentation, and ease-of-use for the function caller, you should always use an exception specification when you write a function that throws exceptions.
5.5.1 unexpected()
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.
5.5.2 set_unexpected()
unexpected() is implemented with a pointer to a function, so you can change its behavior. You do so with a function called set_unexpected() which, like set_new_handler(), takes the address of a function with no arguments and void return value. Also, it returns the previous value of the unexpected() pointer so 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 all the features discussed so far in the chapter:
//: C07:Except.cpp
// Basic exceptions
// Exception specifications & unexpected()
#include <exception>
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
class Up {};
class Fit {};
void g();
void f(int i) throw (Up, Fit) {
switch(i) {
case 1: throw Up();
case 2: throw Fit();
}
g();
}
// void g() {} // Version 1
void g() { throw 47; } // Version 2
// (Can throw built-in types)
void my_unexpected() {
cout << "unexpected exception thrown";
exit(1);
}
int main() {
set_unexpected(my_unexpected);
// (ignores return value)
for(int i = 1; i <=3; i++)
try {
f(i);
} catch(Up) {
cout << "Up caught" << endl;
} catch(Fit) {
cout << "Fit caught" << endl;
}
} ///:~
The classes Up and Fit are created solely to throw as exceptions. Often exception classes will be this small, but sometimes they contain additional information so that the handlers can query them.
f() is a function that 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 then someone changes g() so it throws exceptions and the new g() is linked in with f(). Now f() begins to throw a new exception, unbeknown to the creator of f(). Thus the exception specification is violated.
The my_unexpected() function has no arguments or return value, following the proper form for a custom unexpected() function. It simply prints a message so you can see it has been called, then exits the program. Your new unexpected() function must not return (that is, you can write the code that way but it’s an error). However, it can throw another exception (you can even rethrow the same exception), or call exit() or abort(). If unexpected() throws an exception, the search for the handler starts at the function call that threw the unexpected exception. (This behavior is unique to unexpected())
Although the new_handler() function pointer can be null and the system will do something sensible, the unexpected() function pointer should never be null. The default value is terminate() (mentioned later), but whenever you use exceptions and specifications you should write your own unexpected() to log the error and either rethrow it, throw something new, or terminate the program.
In main(), the try block is within a for loop so all the possibilities are exercised. Note that this is a way to 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 ones the programmer of f() said would be thrown. Version two of g() causes my_unexpected() to be called because f() then throws an int. (You can throw any type, including a built-in type)
In the call to set_unexpected(), the return value is ignored, but it can also be saved in a pointer to function and restored later.
5.5.3 Catching any exception
If your function has no exception specification, any type of exception can be thrown. One solution to this problem is to create a handler that catches any type of exception. You do this using the ellipses in the argument list:
catch(...) {
cout << "an exception was thrown" << endl;
}
This will catch any exception, so you’ll want to put it at the end of your list of handlers to avoid pre-empting any that follow it. The ellipses give you no possibility to have an argument or to know anything about the type of the exception. It’s a catch-all.
Self Assessment Questions
1. In what situation is the special function unexpected() called?
2. How is the scenario wherein a function has no exception specification dealt with in case exceptions occur?
5.6 Rethrowing an Exception
Sometimes you’ll want to rethrow the exception that you just caught, particularly when you use the ellipses to catch any exception because there’s no information available about the exception. This is accomplished by saying throw with no argument:
catch(...) {
cout << "an exception was thrown" << endl;
throw;
}
Any further catch clauses for the same try block are still ignored – the throw causes the exception to go to the exception handlers in the next-higher context. In addition, everything about the exception object is preserved, so the handler at the higher context that catches the specific exception type is able to extract all the information from that object.
5.7 Uncaught Exceptions
If none of the exception handlers following a particular try block matches an exception, that exception moves to the next-higher context, that is, the function or try block surrounding the try block that failed to catch the exception. (The location of this higher-context try block is not always obvious at first glance.) This process continues until, at some level, a handler matches the exception. At that point, the exception is considered “caught,” and no further searching occurs.
If no handler at any level catches the exception, it is “uncaught” or “unhandled.” An uncaught exception also occurs if a new exception is thrown before an existing exception reaches its handler – the most common reason for this is that the constructor for the exception object itself causes a new exception.
5.7.1 terminate()
If an exception is uncaught, the special function terminate() is automatically called. Like unexpected(), terminate is actually a pointer to a function. Its default value is the Standard C library function abort(), which immediately exits the program with no calls to the normal termination functions (which means that destructors for global and static objects might not be called).
No cleanups occur for an uncaught exception; that is, no destructors are called. If you don’t wrap your code (including, if necessary, all the code in main()) in a try block followed by handlers and ending with a default handler (catch(...)) to catch all exceptions, then you will take your lumps. An uncaught exception should be thought of as a programming error.
5.7.2 set_terminate()
You can install your own terminate() function using the standard set_terminate() function, which returns a pointer to the terminate() function you are replacing, so you can restore it later if you want. Your custom terminate() must take no arguments and have a void return value. In addition, any terminate() handler you install must not return or throw an exception, but instead must call some sort of program-termination function. If terminate() is called, it means the problem is unrecoverable. Like unexpected(), the terminate() function pointer should never be null.
Here’s an example showing the use of set_terminate(). Here, the return value is saved and restored so the terminate() function can be used to help isolate the section of code where the uncaught exception is occurring:
//: C07:Trmnator.cpp
// Use of set_terminate()
// Also shows uncaught exceptions
#include <exception>
#include <iostream>
#include <cstdlib>
using namespace std;
void terminator() {
cout << "I'll be back!" << endl;
abort();
}
void (*old_terminate)()
= set_terminate(terminator);
class Botch {
public:
class Fruit {};
void f() {
cout << "Botch::f()" << endl;
throw Fruit();
}
~Botch() { throw 'c'; }
};
int main() {
try{
Botch b;
b.f();
} catch(...) {
cout << "inside catch(...)" << endl;
}
} ///:~
The definition of old_terminate looks a bit confusing at first: It not only creates a pointer to a function, but it initializes that pointer to the return value of set_terminate(). Even though you may be familiar with seeing a semicolon right after a pointer-to-function definition, it’s just another kind of variable and may be initialized when it is defined.
The class Botch not only throws an exception inside f(), but also in its destructor. This is one of the situations that causes a call to terminate(), as you can see in main(). Even though the exception handler says catch(...), which would seem to catch everything and leave no cause for terminate() to be called, terminate() is called anyway, because in the process of cleaning up the objects on the stack to handle one exception, the Botch destructor is called, and that generates a second exception, forcing a call to terminate(). Thus, a destructor that throws an exception or causes one to be thrown is a design error.
Self Assessment Questions
1. What happens when the terminate() function is called?
2. What is the purpose of the set_terminate() function?
5.8 Standard Exceptions
The set of exceptions used with the Standard C++ library are also available for your own 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 what you need, you can derive from it.
The following tables describe the standard exceptions:
exception
The base class for all the exceptions thrown by the C++ standard library. You can ask what() and get a result that can be displayed as a character representation.
logic_error
Derived from exception. Reports program logic errors, which could presumably be detected before the program executes.
runtime_error
Derived from exception. Reports runtime errors, which can presumably be detected only when the program executes.
The iostream exception class ios::failure is also derived from exception, but it has no further subclasses.
The classes in both of the following tables can be used as they are, or they can act as base classes to derive your own more specific types of exceptions.
Exception classes derived from logic_error
domain_error
Reports violations of a precondition.
invalid_argument
Indicates an invalid argument to the function it’s thrown from.
length_error
Indicates an attempt to produce an object whose length is greater than or equal to NPOS (the largest representable value of type size_t).
out_of_range
Reports an out-of-range argument.
bad_cast
Thrown for executing an invalid dynamic_cast expression in run-time type identification
bad_typeid
Reports a null pointer p in an expression typeid(*p).
Exception classes derived from runtime_error
range_error
Reports violation of a postcondition.
overflow_error
Reports an arithmetic overflow.
bad_alloc
Reports a failure to allocate storage.
5.9 Programming with Exceptions
For most programmers, especially C programmers, exceptions are not available in their existing language and take a bit of adjustment. Here are some guidelines for programming with exceptions.
5.9.1 When to avoid exceptions
Exceptions aren’t the answer to all problems. In fact, if you simply go looking for something to pound with your new hammer, you’ll cause trouble. The following sections point out situations where exceptions are not warranted:
· Not for asynchronous events – The Standard C signal() system, and any similar system, handles asynchronous events: events that happen outside the scope of the program, and thus events the program cannot anticipate. C++ exceptions cannot be used to handle asynchronous events because the exception and its handler are on the same call stack. That is, exceptions rely on scoping, whereas asynchronous events must be handled by completely separate code that is not part of the normal program flow (typically, interrupt service routines or event loops). This is not to say that asynchronous events cannot be associated with exceptions. But the interrupt handler should do its job as quickly as possible and then return. Later, at some well-defined point in the program, an exception might be thrown based on the interrupt.
· Not for ordinary error conditions – If you have enough information to handle an error, it’s not an exception. You should take care of it in the current context rather than throwing an exception to a larger context. Also, C++ exceptions are not thrown for machine-level events like divide-by-zero. It’s assumed these are dealt with by some other mechanism, like the operating system or hardware. That way, C++ exceptions can be reasonably efficient, and their use is isolated to program-level exceptional conditions.
· Not for flow-of-control – An exception looks somewhat like an alternate return mechanism and somewhat like a switch statement, so you can be tempted to use them for other than their original intent. This is a bad idea, partly because the exception-handling system is significantly less efficient than normal program execution; exceptions are a rare event, so the normal program shouldn’t pay for them. Also, exceptions from anything other than error conditions are quite confusing to the user of your class or function.
· You’re not forced to use exceptions – Some programs are quite simple, many utilities, for example. You may only need to take input and perform some processing. In these programs you might attempt to allocate memory and fail, or try to open a file and fail, and so on. It is acceptable in these programs to use assert() or to print a message and abort() the program, allowing the system to clean up the mess, rather than to work very hard to catch all exceptions and recover all the resources yourself. Basically, if you don’t need to use exceptions, you don’t have to.
· New exceptions, old code - Another situation that arises is the modification of an existing program that doesn’t use exceptions. You may introduce a library that does use exceptions and wonder if you need to modify all your code throughout the program. Assuming you have an acceptable error handling scheme already in place, the most sensible thing to do here is surround the largest block that uses the new library (this may be all the code in main()) with a try block, followed by a catch(...) and basic error message. You can refine this to whatever degree necessary by adding more specific handlers, but, in any case, the code you’re forced to add can be minimal. You can also isolate your exception-generating code in a try block and write handlers to convert the exceptions into your existing error-handling scheme. It’s truly important to think about exceptions when you’re creating a library for someone else to use, and you can’t know how they need to respond to critical error conditions.
5.9.2 Using exceptions
· Use exceptions to:
- Fix the problem and call the function (which caused the exception) again.
- Patch things up and continue without retrying the function.
- Calculate some alternative result instead of what the function was supposed to produce.
- Do whatever you can in the current context and rethrow the same exception to a higher context.
- Do whatever you can in the current context and throw a different exception to a higher context.
- Terminate the program.
- Make your library and program safer. This is a short-term investment (for debugging) and a long-term investment (for application robustness).
· Always 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 may come out of this function. Of course, you can’t always anticipate by looking at the code what 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’ll get a call to unexpected(). Anytime you use exception specifications or call functions that do, you should create your own unexpected() function that logs a message and rethrows the same exception.
· 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 for your users if they 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 very good idea to nest the exception classes inside 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 namespace. You can nest your exceptions even if you’re deriving them from C++ standard exceptions.
· Use exception hierarchies – Exception hierarchies provide a valuable way to classify the different types of critical errors that may 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 baseclass handler will catch the new exception. Of course, the Standard C++ exceptions are a good example of an exception hierarchy, and one that you can use to build upon.
· Catch by reference, not by value – If you throw an object of a derived class and it is caught by value in a handler for an object of the base class, that object is “sliced” – that is, the derived-class elements are cut off and you’ll end up with the base-class object being passed. Chances are this is not what you want because the object will behave like a base-class object and not the derived class object it really is (or rather, was – before it was sliced). Here’s an example:
//: C07:Catchref.cpp
// Why catch by reference?
#include <iostream>
using namespace std;
class Base {
public:
virtual void what() {
cout << "Base" << endl;
}
};
class Derived : public Base {
public:
void what() {
cout << "Derived" << endl;
}
};
void f() { throw Derived(); }
int main() {
try {
f();
} catch(Base b) {
b.what();
}
try {
f();
} catch(Base& b) {
b.what();
}
} ///:~
Output:
Base
Derived
because, when the object is caught by value, it is turned into a Base object (by the copy constructor) and must behave that way in all situations, whereas when it’s caught by reference, only the address is passed and the object isn’t truncated, so it behaves like what it really is, a Derived in this case. 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 may have occurred from heap exhaustion. If you throw exception objects, the exception-handling system takes care of all storage.
· 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, it means that a new exception may be thrown before the catch-clause for an existing exception is reached, which will cause a call to terminate(). This means that if you call any functions inside a destructor that may 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.
5.10 Summary
Error recovery is a fundamental concern for every program you write, and it’s especially important in C++, where one of the goals is to create program components for others to use. To create a robust system, each component must be robust. The goals for exception handling in C++ are to simplify the creation of large, reliable programs using less code than currently possible, with more confidence that your application doesn’t have an unhandled error. This is accomplished with little or no performance penalty, and with low impact on existing code. Basic exceptions are not terribly difficult to learn, and you should begin using them in your programs as soon as you can. Exceptions are one of those features that provide immediate and significant benefits to your project.
5.11 Terminal Questions
1. What is the purpose of exception handling?
2. What do you infer from the phrase, “Throwing an exception”?
3. Describe the structure of an exception handling process with the help of an example.
4. Describe the two basic exception handling models.
5. Why is writing exception specification recommended?
6. Why would you want to rethrow an exception?
7. What are the similarities between unexpected() and terminate()?
8. Explain standard exceptions?
9. Describe the scenarios in which using exceptions must be avoided.
10. What are the application areas of exception handling?

No comments:

Post a Comment