Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
In most of the examples in these volumes, we use assert( )
as it was intended: for debugging during development with code that can be
disabled with #define NDEBUG for the shipping product. Runtime
error checking uses the require.h functions (assure( ) and require( ))
developed in Chapter 9 in Volume 1 and repeated here in Appendix B. These
functions are a convenient way to say, There s a problem here you ll probably
want to handle with some more sophisticated code, but you don t need to be
distracted by it in this example. The require.h functions might be
enough for small programs, but for complicated products you ll want to write
more sophisticated error-handling code.
Error handling is quite straightforward when you know
exactly what to do, because you have all the necessary information in that
context. You can just handle the error at that point.
The problem occurs when you don t have enough
information in that context, and you need to pass the error information into a
different context where that information does exist. In C, you can handle this
situation using three approaches:
1. Return error information from the function or, if the return
value cannot be used this way, set a global error condition flag. (Standard C
provides errno and perror( ) to support this.) As mentioned
earlier, the programmer is likely to ignore the error information because
tedious and obfuscating error checking must occur with each function call. In
addition, returning from a function that hits an exceptional condition might
not make sense.
2. Use the little-known Standard C library signal-handling system,
implemented with the signal( ) function (to determine what happens
when the event occurs) and raise( ) (to generate an event). Again,
this approach involves high coupling because it requires the user of any
library that generates signals to understand and install the appropriate
signal-handling mechanism. In large projects the signal numbers from different
libraries might clash.
3. Use the nonlocal goto functions in the Standard C library:
setjmp( ) and longjmp( ). With setjmp( )
you save a known good state in the program, and if you get into trouble, longjmp( )
will restore that state. Again, there is high coupling between the place where
the state is stored and the place where the error occurs.
When considering error-handling schemes with C++, there s an
additional critical problem: The C techniques of signals and setjmp( )/longjmp( )
do not call destructors, so objects aren t properly cleaned up. (In fact, if longjmp( )
jumps past the end of a scope where destructors should be called, the behavior
of the program is undefined.) This makes it virtually impossible to effectively
recover from an exceptional condition because you ll always leave objects
behind that haven t been cleaned up and that can no longer be accessed. The
following example demonstrates this with setjmp/longjmp:
//: C01:Nonlocal.cpp
// setjmp() & longjmp().
#include <iostream>
#include <csetjmp>
using namespace std;
class Rainbow {
public:
Rainbow() { cout << "Rainbow()"
<< endl; }
~Rainbow() { cout << "~Rainbow()"
<< endl; }
};
jmp_buf kansas;
void oz() {
Rainbow rb;
for(int i = 0; i < 3; i++)
cout << "there's no place like
home" << endl;
longjmp(kansas, 47);
}
int main() {
if(setjmp(kansas) == 0) {
cout << "tornado, witch,
munchkins..." << endl;
oz();
} else {
cout << "Auntie Em! "
<< "I had the strangest
dream..."
<< endl;
}
} ///:~
The setjmp( ) function is odd because if you
call it directly, it stores all the relevant information about the current
processor state (such as the contents of the instruction pointer and runtime
stack pointer) in the jmp_buf and returns zero. In this case it behaves
like an ordinary function. However, if you call longjmp( ) using
the same jmp_buf, it s as if you re returning from setjmp( )
again you pop right out the back end of the setjmp( ). This time,
the value returned is the second argument to longjmp( ), so you can
detect that you re actually coming back from a longjmp( ). You can
imagine that with many different jmp_bufs, you could pop around to many
different places in the program. The difference between a local goto
(with a label) and this nonlocal goto is that you can return to any
pre-determined location higher up in the runtime stack with setjmp( )/longjmp( )
(wherever you ve placed a call to setjmp( )).
The problem in C++ is that longjmp( ) doesn t
respect objects; in particular it doesn t call destructors when it jumps out of
a scope. Destructor
calls are essential, so this approach won t work with C++. In fact, the C++
Standard states that branching into a scope with goto (effectively
bypassing constructor calls), or branching out of a scope with longjmp( )
where an object on the stack has a destructor, constitutes undefined behavior.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |