Item 13: Catch exceptions by reference.
When you write a catch
clause, you must specify how exception objects are to be passed to that clause. You have three choices, just as when specifying how parameters should be passed to functions: by pointer, by value, or by
Let us consider first catch by pointer. In theory, this should be the least inefficient way to implement the invariably slow process of moving an exception from throw
site to catch
clause (see Item 15). That's because throw by pointer is the only way of moving exception information without copying an object (see Item 12). For
class exception { ... }; // from the standard C++ // library exception // hierarchy (see Item 12) void someFunction() { static exception ex; // exception object ... throw &ex; // throw a pointer to ex ... } void doSomething() { try { someFunction(); // may throw an exception* } catch (exception *ex) { // catches the exception*; ... // no object is copied } }
This looks neat and tidy, but it's not quite as well-kept as it appears. For this to work, programmers must define exception objects in a way that guarantees the objects exist after control leaves the functions throwing pointers to them. Global and static objects work fine, but it's easy for programmers to forget the constraint. If they do, they typically end up writing code like
void someFunction() { exception ex; // local exception object; // will be destroyed when // this function's scope is ... // exited throw &ex; // throw a pointer to an ... // object that's about to } // be destroyed
This is worse than useless, because the catch
clause handling this exception receives a pointer to an object that no longer
An alternative is to throw a pointer to a new heap
void someFunction() { ... throw new exception; // throw a pointer to a new heap- ... // based object (and hope that } // operator new see Item 8 // doesn't itself throw an // exception!)
This avoids the I-just-caught-a-pointer-to-a-destroyed-object problem, but now authors of catch
clauses confront a nasty question: should they delete the pointer they receive? If the exception object was allocated on the heap, they must, otherwise they suffer a resource leak. If the exception object wasn't allocated on the heap, they mustn't, otherwise they suffer undefined program behavior. What to
It's impossible to know. Some clients might pass the address of a global or static object, others might pass the address of an exception on the heap. Catch by pointer thus gives rise to the Hamlet conundrum: to delete or not to delete? It's a question with no good answer. You're best off ducking
Furthermore, catch-by-pointer runs contrary to the convention established by the language itself. The four standard exceptions bad_alloc
(thrown when operator
new
(see Item 8) can't satisfy a memory request), bad_cast
(thrown when a dynamic_cast
to a reference fails; see Item 2), bad_typeid
(thrown when dynamic_cast
is applied to a null pointer), and bad_exception
(available for unexpected exceptions; see Item 14) are all objects, not pointers to objects, so you have to catch them by value or by reference,
Catch-by-value eliminates questions about exception deletion and works with the standard exception types. However, it requires that exception objects be copied twice each time they're thrown (see Item 12). It also gives rise to the specter of the slicing problem, whereby derived class exception objects caught as base class exceptions have their derivedness "sliced off." Such "sliced" objects are base class objects: they lack derived class data members, and when virtual functions are called on them, they resolve to virtual functions of the base class. (Exactly the same thing happens when an object is passed to a function by value see Item E22.) For example, consider an application employing an exception class hierarchy that extends the standard
class exception { // as above, this is a public: // standard exception class virtual const char * what() throw(); // returns a brief descrip. ... // of the exception (see // Item 14 for info about }; // the "throw()" at the // end of the declaration) class runtime_error: // also from the standard public exception { ... }; // C++ exception hierarchy class Validation_error: // this is a class added by public runtime_error { // a client public: virtual const char * what() throw(); // this is a redefinition ... // of the function declared }; // in class exception above void someFunction() // may throw a validation { // exception ... if (a validation test fails) { throw Validation_error(); } ... } void doSomething() { try { someFunction(); // may throw a validation } // exception catch (exception ex) { // catches all exceptions // in or derived from // the standard hierarchy
cerr << ex.what(); // calls exception::what(), ... // never } // Validation_error::what() }
The version of what
that is called is that of the base class, even though the thrown exception is of type Validation_error
and Validation_error
redefines that virtual function. This kind of slicing behavior is almost never what you
That leaves only catch-by-reference. Catch-by-reference suffers from none of the problems we have discussed. Unlike catch-by-pointer, the question of object deletion fails to arise, and there is no difficulty in catching the standard exception types. Unlike catch-by-value, there is no slicing problem, and exception objects are copied only
If we rewrite the last example using catch-by-reference, it looks like
void someFunction() // nothing changes in this { // function ... if (a validation test fails) { throw Validation_error(); } ... } void doSomething() { try { someFunction(); // no change here } catch (exception& ex) { // here we catch by reference // instead of by value cerr << ex.what(); // now calls // Validation_error::what(), ... // not exception::what() } }
There is no change at the throw
site, and the only change in the catch
clause is the addition of an ampersand. This tiny modification makes a big difference, however, because virtual functions in the catch
block now work as we expect: functions in Validation_error
are invoked if they redefine those in exception
.
What a happy confluence of events! If you catch by reference, you sidestep questions about object deletion that leave you damned if you do and damned if you don't; you avoid slicing exception objects; you retain the ability to catch standard exceptions; and you limit the number of times exception objects need to be copied. So what are you waiting for? Catch exceptions by