This is the third and final part of my article on exceptions. In this part we deal with:-
-
Dangers of exceptions
-
Standard Library exceptions
-
Guidelines.
Resource recovery is important in the face of errors. An acquired resource can easily be lost during stack unwinding unless it was acquired automatically (that is to say, not as a pointer). However, it is a common idiom in C++ to acquire resources by pointer.
To be exception safe, pointers, resource handles, important state variables (for example a "top of stack" if you're implementing a stack template class) need special attention. It's possible to provide that attention with specially written try(){…} catch() clauses. It is much easier to rely on C++'s constructor / destructor mechanism.
void f(const char *filename) { FILE *f = fopen(filename, "r"); try { // use f fclose(f); } catch (...) { fclose(f); throw; // rethrow the exception } }
In the above example, the resource (file) is acquired, then used. An exception may be thrown (not necessarily by the file specific code), so a handler is present to clean up the mess. This seems reasonable enough, but what happens if we have a lot of resources in one function? (critical sections, files, locks, events). It clearly gets too messy.
This is changing in the face of exceptions. Smart pointers are classes which acquire the resource pointer in the constructor, and safely release in the destructor. The pointer is accessed through an overloaded member access operator, or custom conversion.
class File_ptr { public: File_ptr(const char* name, const char *mode ) { fp = fopen(name, mode);} ~File_ptr() {fclose( fp); } operator FILE*(){return fp;} private: FILE *fp; // not implemented File_ptr( const File_ptr& ); File_ptr& operator=(const File_ptr& ); }
Now we use the atomic variable of class File_ptr where we would've used a pointer to FILE. In the case of an exception occurring before the File_ptr goes out of scope, stack unwinding takes care of the cleanup for you - you don't need special exception handlers for every resource.
For pointers created and destroyed by new and delete, STL provides the auto_ptr<> template. The resources can be allocated when the object is constructed (this is optional), the resources will be released when the object is deleted. (Note - consult your system documentation about the precise behaviour of auto_ptr<>).
The idiom of wrapping a raw pointer with an object to control its initialisation and release is called "resource acquisition is initialisation", and is covered on the QA Training Advanced C++ course, in Scott Meyers' "More Effective C++" (Item 9), and in Bjarne Stroustrup's "The C++ Programming Language" (14.4), where the phrase was coined.
Exceptions can be expensive to throw. Consider the difference between the following handlers:
Thing t; try { throw t; // 1 } catch (Thing x){ // 2 throw; } catch (Thing& x){ // 3 // whatever }
At point 1, a temporary copy of the local object is thrown, not the local object itself. The copy is necessary to extend the exception's lifetime until a handler has completed. The cost of this copy is one copy constructor.
The handler at point 2 catches by value; a copy is made of the thrown exception object. The cost of this is another copy constructor.
The handler at point 3 catches by reference; no copy is made.
Another reason not to catch by value is that of polymorphism. Consider this:
class A { /* .. */ }; class B : public A { /* .. */ }; void f() { try { throw B; } catch (A a) { // # throw a; // $ } }
At the point #, the exception object has been caught by value of type A (base class of the originally thrown type, B). At the point $, the exception is rethrown, except, the exception thrown now is of type A - we've lost our derived class information!
There is a good argument against throwing a pointer too. Should the handler delete the pointer? Clearly, catching by reference, or preferably const-reference is the best way of handling exceptions.
Destructors are called in three situations:
-
when the object goes out of scope or is deleted.
-
during the stack unwinding mechanism of exception handling.
-
by explicit call (rare).
Therefore, there may or may not be an exception active when a destructor is called. If another exception is thrown whilst an exception is already active, the program calls terminate(). This is not an acceptable outcome if you've decided to use exceptions to manage your programs (and exceptions have to be considered right through your code when you use them - no half measures).
There are two solutions to this situation:
-
use uncaught_exception() in the destructor to determine if an exception is active, and only throw if there isn't, or
-
never throw exceptions from destructors.
On the surface the first option may seem fine, but as Steve Clamage put it: "My question is why would it be good design to throw the exception only sometimes, and if you can sometimes get along without throwing it, why not always get along without throwing it?" - Steve Clamage, Sun Microsystems.
The only reasonable solution is never throw from destructors. This guideline is supported by all major commentators.
What? You mean they got constructors too? Well, if you've been paying attention so far, the intricacies of exceptions from constructors shouldn't be a problem to you.
The C++ Standard (15.2.2 [except.ctor]) says, "An object that is partially constructed or partially destroyed will have destructors executed for all of its fully constructed subobjects…".
If you've wrapped your resources with smart pointers, this shouldn't be a problem, right?
Exception handling (like all error-handling code) comes with costs. Programs will be slower and of a larger size when using exceptions; they have a lot more bookkeeping to do. Having try-blocks in your program may increase your code size and runtime cost by 5-10% (source: More Effective C++). To minimise this, avoid unnecessary try-blocks.
Exception specifications add a similar cost, but since we're not using those, that doesn't matter.
So how much of a hit do we take when we actually throw an exception? The best answer we can give is "a big one". Meyers suggests returning from a function by throwing an exception could cost up to three times what it does when returning normally. My own test with MS VC++ 5 showed a fourfold increase in run time.
This overhead will come down in time. In any case, for most programs it's more important to get the correct answer a bit slower, than to get the wrong answer fast. It is even conceivable that code with exception handling constructs could execute faster than old style error handling code, since a lot of if-else style constructs could be removed.
The standard C++ library defines a base class for the types of objects thrown as exceptions by C++ Standard library components in the header <exception>:
namespace std { class exception { public: exception() throw(); exception(const exception&) throw(); exception& operator=(const exception&) throw(); virtual ~exception() throw(); virtual const char* what() const throw(); }; }
The class bad_exception is also defined in <exception>. This exception is thrown when an exception specification has been compromised, but the exception specification allows exceptions of type bad_exception to be thrown. std::unexpected() performs the necessary remapping.
The following standard exceptions are defined in the header <stdexcept>:
logic_error domain_error invalid_argument length_error out_of_range
(the indented classes are publicly derived from logic_error).
runtime_error range_error overflow_error underflow_error
(the indented classes are publicly derived from runtime_error).
A logic_error is an error that is detectable in the logic of the code. A runtime_error is one that is not detected in the logic of the code.
-
catch by reference (or const reference) (8.2)
-
use the "resource acquisition is initialisation" idiom (8.1)
-
use exceptions only for exceptional circumstances, where other error handling techniques won't suffice
-
treat exceptions as another area of program design
-
make constructors & destructors exception safe using function try blocks (7.1)
The C++ Programming Language Third Edition Bjarne Stroustrup (Addison-Wesley, 1997)
More Effective C++ Scott Meyers (Addison-Wesley 1996)
The ISO/IEC C++ Language Standard (14882-1998) (ISO/IEC 1998)
Exception Handling: A False Sense of Security Tom Cargill (C++ Report, Volume 6, Number 9, Nov-Dec 1994)
Taligent's Guide to Programming Taligent (Addison-Wesley, 1995)
Counting Object in C++ (Sidebar: Placement new and placement delete) Scott Meyers (C/C++ Users Journal, April 1998) Downloadable from http://meyerscd.awl.com/
Overload Journal #35 - Jan 2000 + Programming Topics
Browse in : |
All
> Journals
> Overload
> 35
(8)
All > Topics > Programming (877) Any of these categories - All of these categories |