Bruce Eckel's Thinking in C++, 2nd Ed Contents | Prev | Next

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.

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.

Typical uses of exceptions

Do use exceptions to

  1. Fix the problem and call the function (which caused the exception) again.
  2. Patch things up and continue without retrying the function.
  3. Calculate some alternative result instead of what the function was supposed to produce.
  4. Do whatever you can in the current context and rethrow the same exception to a higher context.
  5. Do whatever you can in the current context and throw a different exception to a higher context.
  6. Terminate the program.
  7. Wrap functions (especially C library functions) that use ordinary error schemes so they produce exceptions instead.
  8. Simplify. If your exception scheme makes things more complicated, then it is painful and annoying to use.
  9. 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 base-class 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.

Multiple inheritance

You’ll remember from Chapter XX that the only essential place for MI is if you need to upcast a pointer to your object into two different base classes – that is, if you need polymorphic behavior with both of those base classes. It turns out that exception hierarchies are a useful place 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

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:

//: C23: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();
  }
} ///:~

The output is

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.

Throw exceptions in constructors

Because a constructor has no return value, you’ve previously had two choices to report an error during construction:

  1. Set a nonlocal flag and hope the user checks it.
  2. Return an incompletely created object and hope the user checks it.
This is a serious problem because C programmers have come to rely on an implied guarantee that object creation is always successful, which is not unreasonable in C where 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, 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.

Avoid naked pointers

See Wrapped.cpp. 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 won’t be released if an exception is thrown in the constructor.

Contents | Prev | Next


Contact: webmaster@codeguru.com
CodeGuru - the website for developers.
[an error occurred while processing this directive]