This is not quite what you have asked for, and I dropped comp.lang.c++
from the list because it has no direct C++ implications. I also
include a bit of detail on setjmp() and longjmp().
... words or flowchart to explain program flow. I mean C books have
those railroad diagrams for trivial things like sequence-selection-
iteration-continue-break etc. Can we see a clear program flow for
[exceptions]?
This is somewhat difficult, as part of the point of exceptions is
to obscure program flow.
(Not really, but sort of.)
Languages with exceptions often, but not always, have some kind
of "exception protection" mechanism. C++ has one but it mostly
hidden. Lisps tend to have an explicit version called "unwind
protect". The explicit version is relatively easy to explain,
simply by pretending that *every* function call *always* has one
of these things going on -- even if the function does not have
a return value.
Take any ordinary C function, with a return value of type T (if
the function has type "void" pretend it returns "int" for the moment).
Replace:
T f(parameters) {
...
if (cond) return val1;
...
return val2;
}
with:
struct T2 { int normal_return; T val; };
struct T2 f(parameters) {
struct T2 rval;
rval.normal_return = 1;
...
if (cond) { rval.val = val1; return rval; }
...
rval.val = val2; return val2;
}
Notice that *every* "return" from f() now returns *two* values,
one indicating "normal return" and the other giving the actual
value.
Now, every call to f() has to change as well. Instead of:
T result;
...
result = f(arguments);
...
we have:
struct T2 result_pair;
...
result_pair = f(arguments);
if (result_pair.normal_return == 0) {
/* this new code is for exceptions */
...
}
result = result_pair.val;
In other words, we now have to test, after every call, to see
whether the function "returned normally" -- in which case we go
on with normal program flow and everything works as before -- or
whether f() had some kind of exception occur.
If f() chooses to use the new exception mechanism, it "throws" by
doing this:
struct T2 f(parameters) {
struct T2 rval;
rval.normal_return = 1;
...
if (decide_to_bail_out) {
rval.normal_return = 0; /* "exception" return */
return T2;
}
...
rval.val = val2; return val2; /* "normal" return */
}
Now suppose f() itself calls some other function g(). Function g()
has a normal-return value, and also an exception-return:
struct T3 g(params);
struct T2 f(parameters) {
struct T2 rval;
struct T3 g_value;
rval.normal_return = 1;
...
g_value = g(args);
if (g_value.normal_return == 0) {
/* g() "threw an exception" */
/* we have a chance to catch it here, but by default we just: */
rval.normal_return = 0;
return rval;
}
...
if (decide_to_bail_out) {
rval.normal_return = 0; /* this time we do the "throw" */
return rval;
}
...
rval.val = val2; return val2; /* "normal" return */
}
Again, *every call*, everywhere in the program, *always* has to
check to see if the function it just called is using one of these
"sideways" exception-return tricks. If so, it has a chance to
"catch" the exception right then and there, or to continue to
"throw" it up to its own caller.
Even functions that know or care nothing about exceptions have
to check, because any function *they* call could use exceptions
-- they are built into the language. Every time a function
returns, it either "returns normally" and the program goes on,
or it "returns sideways" with an exception and you get a chance
to clean up.
The "chance to clean up" is the unwind-protect:
unwind_protect {
f();
g();
h();
} on_exception {
something;
}
more;
turns into:
if (f() returns sideways) goto unwind_protect;
if (g() returns sideways) goto unwind_protect;
if (h() returns sideways) goto unwind_protect;
goto around;
unwind_protect:
something;
around:
more;
The unwind-protect sequence is often spelled "catch". In languages
that have "catch", they may (or may not) require you to list the
particular exception(s) caught. Ultimately, however, it means that
every function call everywhere in the program has two ways out of
it: the normal one, and the "sideways" exception.
The problem with this kind of explicit implementation (which you-the-
programmer can do in C, as long as you get to rewrite every function
call) is that it tends to run significantly slower than one in
which functions do not have "rval.normal_return = 1"s everywhere.
Thus, much of the effort in implementing exceptions goes into
inventing tricks that avoid slowing down every function call and
return. As it turns out, in languages like C++ (with destructors
for class objects that are allocated at function entry), this does
not always buy as much of a speed improvement as one might like,
because -- in effect -- every function already uses exceptions,
just so that it can catch them with an unwind-protect, destroy any
local objects, then continue up the "sideways exception return"
chain to the next function that is actually catching exceptions --
which is in fact the very next function up.
(I have occasionally thought that simply dedicating one more
machine register to every function call, for a "sideways exception
return" flag, might be easier than all the gimmicks one finds
in actual compilers, which tend to leave massive tables for
the exception code to pore over at runtime just to avoid
explicit "<r1,r2> = f(); if (r2) goto unwind; else val = r1;"
code. With branch prediction, one could even label the "goto if
r2" as a predicted-false branch, and have it disappear into the
CPU's parallelism on non-exception returns.)
C++'s "try" is an explicit unwind-protect, while C++'s object
destructors require an automatic, implicit unwind-protect. Either
way, it causes every sub-function to be able to "return" in one
of two ways: the normal way, or the "sideways" unwind way.
Languages that provide exception *values* (there are some with that
offer no "value" component) have to pass the value up the call
chain somehow. If one were using a machine register (like r2 in
my example above), one might dedicate the integer value 0 to mean
"no exception" and allow any other integer or pointer value as the
exception-value. In C, we might express this by replacing the
"struct"s that functions return with:
struct T2 {
int exception; /* 0 => normal return, nonzero => exception value */
T val;
};
and instead of setting "rval.normal_return = 1", set
"rval.exception = 0". To use a pointer, change this to "void *"
instead of "int". Now:
throw 72;
becomes:
rval.exception = 72;
return rval;
or:
throw("oops");
becomes:
rval.exception = "oops";
return rval;
Again, all it really means is "set the flag that says we want to
`return sideways' instead of returning normally, then return normally
and force the caller to deal with it all". The caller *has* to
deal with it, even if that caller is not using exceptions. Any
sneaky trick that a compiler-writer uses to try to speed this up
is merely an optimization -- logically, each caller gets a chance
to "protect" against this "unwinding" of the call stack.
The simulations people write in C using setjmp/longjmp tend to use
a global variable or two to keep linked list of "places that catch"
(longjmp labels) and "caught exception-value".
Note that C compilers could -- though few if any do -- even implement
setjmp() and longjmp() in terms of this same sort of "regular" vs
"sideways" return. The reason most do not is that longjmp is
*extremely* restricted, so that it can often be implemented by an
ugly hack: have setjmp() save the machine's program counter(s) and
stack/frame pointer(s), and have longjmp() restore those. The
problem with this hack is that it can interfere badly wth things
like variable-length arrays, alloca() functions (which are not part
of Standard C), and optimization. This problem is the reason the
C standard goes all wobbly on the programmer:
7.10.2.1 The longjmp function
...
[#3] All accessible objects have values as of the time
longjmp was called, except that the values of objects of
automatic storage duration that are local to the function
containing the invocation of the corresponding setjmp macro
that do not have volatile-qualified type and have been
changed between the setjmp invocation and longjmp call are
indeterminate.
...
Examples
[#5] The longjmp function that returns control back to the
point of the setjmp invocation might cause memory associated
with a variable length array object to be squandered.
#include <setjmp.h>
jmp_buf buf;
void g(int n);
void h(int n);
int n = 6;
void f(void)
{
int x[n]; // OK, f is not terminated.
setjmp(buf);
g(n);
}
void g(int n)
{
int a[n]; // a may remain allocated.
h(n);
}
void h(int n)
{
int b[n]; // b may remain allocated.
longjmp(buf,2); // might cause memory loss.
}
In short, longjmp() is the goto they told you not to use in Pascal,
if you were took computer programming / informatics classes at a
University where they used Pascal.
If longjmp() were to be implemented as an exception, with the
functions involved doing "unwind-protect"s, these issues would go
away -- but then C would be heavier-weight than it is, because (as
I explained above) adding exceptions usually means that *every*
function call in *every* program has to go a little bit slower, to
account for "sideways" exception-returns. The trickery you may
read about in C++ books -- describing "searching up a call stack",
for instance -- is all about trying to pay the speed cost only when
an exception is actually used. In C, this might pay off, because
setjmp() and longjmp() are (one hopes) rarely used. In C++, with
object destructors causing implicit "catch"es, it is not at all
obvious to me when this is worthwhile.