Exception Handling -------------------- Control abstractions studied so far are explicit: At the statement involving transfer of control, there is a syntactic indication of the point of transfer. Even for procedure calls or goto statements, there is an explicit indication of the target of transfer (identified by a procedure name or a label name). An implicit control abstraction involves constructs that enable one to set up the transfer point in advance. At the statement that transfers control, the target is not explicitly specified. Examples: (a) function pointers: if a pointer to a function is passed as a parameter to another function g, and g calls this function, we cannot immediately identify the function being called by just examining g. Instead, we need to examine the statement invoking g, where the target function would be specified. (b) return statements: The target of return has already been set up by the caller of the procedure (c) exceptions: when errors or unusual conditions arise during the execution of a function (e.g., divide by zero, null pointer access), control is transferred to a handler (which is a code fragment) for the exception. The handler is not specified at the point where the exception arises, but is in fact set up previously. Terminology: ------------- exception: an error, or more generally, an unusual condition raise, throw, signal: a statement is said to "raise" (or "throw" or "signal") an exception if the execution of this statement leads to an exception. ("throw" is the term used in C++/Java language descriptions, "raise" is used in SML.) catch: a catch statement is used in C++/Java to declare a handler. The keyword "handle" is used in SML to denote the same concept. resumption model: In a resumption model for handling exceptions, the exception transfers control to the handler. After the execution of the handler, control returns back to the statement that raised the exception. Example: signal handling in UNIX/C termination model: In this model, the execution of the statement/function causing the exception is terminated, i.e., control does not return to that statement after the handler is executed. Example: Exception handling in most programming languages, including C++, Java and SML. Exception Handling in SML -------------------------- Exceptions are like datatypes in many ways. They are declared using: exception BadN; They may take arguments, such as: exception BadM of string * int * int * int; Once defined, they may be raised in functions as follows: # let rec comb(n, m) = if n<0 then raise BadN else if m<0 then raise (BadM ("M less than zero", 0, n, m)) else if m>n then raise (BadM ("M > N", 1, n, m)) else if (m=0) || (m=n) then 1 else comb(n-1,m) + comb(n-1,m-1);; # comb(2, -1);; Exception: BadM ("M less than zero", 0, 2, -1). # comb(-1,9);; Exception: BadN. We can set up handlers for exceptions using a "try/with" block: ::= try with ::= | .... | ::= -> The meaning of expressions with handlers is given as folows. First, we say that any function can return a value in its output type, or it can return an exception. If the evaluates without raising an exception, then its value is returned as the value of . If the evaluation of some function f in returns an exception value EV, then the rest of is not evaluated. Instead, EV is matched against the associated with each of the 's. If it matches an , then the corresponding is executed. If there is no match, EV is returned as the value of the expression This semantics implies that uncaught exceptions are propagated up the call tree; if a callee raises an exception to which there is no handle, then the caller gets that exception. If there is a handle for that exception in that caller, that handler is executed; otherwise, the caller of that function will get the exception and so on. Thus, if function f called g and g in turn called h, then any uncaught exception in g is returned to g and if g doesn't catch it, it is returned to f. The semantics of matching exception handlers is exactly as with function defintions. In particular, when there are multiple matches, the first match is taken. The use of exception handlers is illustrated with an example below: # let f m n = try comb(n, m) with BadN -> 1 | BadM(s, 0, x, y) -> (print_string("BadM exception `"); print_string(s); print_string("' raised, ignoring\n"); 1);; # f (-1) 2;; BadM exception `M less than zero' raised, ignoring - : int = 1 # f 1 (-2);; - : int = 1 # f 3 1;; Exception: BadM ("M > N", 1, 1, 3). Exception Handling in C++/Java ---------------------------------- Exception handling in C++/Java is modeled after the way exceptions were designed in SML. The syntactic constructs for exceptions parallel those of SML, and semantics of exceptions remains essentially the same. Syntax: ::= try ::= ... ::= catch () { } example: int fac(int n) { if (n <= 0) throw (-1) ; else if (n > 15) throw ("n too large"); else return n*fac(n-1); } void g (int n) { int k; try { k = fac (n) ; } catch (int i) { cout << "negative value invalid" ; } catch (char *s) { cout << s; } catch (...) { cout << "unknown exception" ; } use of g (-1) will print "negative value invalid" g (16) will print "n too large" If an unexpected error were to arise in evaluation of fac or g, such as running out of memory, then "unknown excpetion" will be printed (provided there is enough memory for successful execution of the handler code.) Exception Vs Return Codes ---------------------------- Exceptions are often used to communucate error values from a callee to its caller. Return values provide alternate means of communicating errors. example use of exception handler float g (int a, int b, int c) { float x = 1./fac(a) + 1./fac(b) + 1./fac(c) ; return x ; } main() { try { g(-1, 3, 25); } catch (char *s) { cout << "Exception `" << s << "'raised, exiting\n"; } catch (...) { cout << "Unknown exception, eixting\n"; } The key point is that we do not need to concern ourselves with every point in the program where an error may arise. Instead, exceptions may be handled at "higher levels" of the program. In the simplest case, they may be handled at the top level, as in the example above. In contrast, if return codes were used to indicate errors, then we are forced to check return codes (and take appropriate action) at every point in code. To illustrate this, suppose that the fac function were modified so that it indicates errors by returning zero, while nonzero return values indicate normal execution. Now, function g and main would have to be modified as follows in order to incorporate error checking: float g(int a, int b, int c) { int x1 = fac(a) ; if (x1 != 0) { int x2 = fac(b) ; if (x2 != 0) { int x3 = fac(c) ; if (x3 != 0) { return 1./x1 + 1./x2 + 1./x3 ; } } } return 0 ; // there was an error } main() { int x = g(-1, 2, 25); if (x < 0) { /* identify where error occurred, print appropriate message */ } } We have also had to modify g so that it returns zero on errors. Note that when errors arise, g is not handling the error, but simply propagates it up. We have had to modify g to introduce so many error-checking statements, even though g is not really handling the error! This sort of "passing through" of error conditions happens for free with exceptions. Specifically, suppose that a function h calls g1, which in turn calls g2 and so on, until finally, function gn calls f. Also suppose that f raises an exception which is handled by h. Then we may need some code in f and h respectively to raise and handle the exception. Functions g1,...,gn need no additional code. In contrast, if we used error return codes, we would need to modify all of g1 through gn (in a manner similar to way we modified g above). Moreover, for each of f, g1,...,gn, we need to identify a suitable return value to indicate errors that is different from any value that function may return after normal execution. The net result is that error-handling becomes very cumbersome when return codes are used to indicate errors. Consequently, programmers frequently omit error-checking, thereby writing code that is prone to failure. Use of Exceptions in C++ Vs Java ---------------------------------- In C++, exception handling was added as an after-thought. Earlier versions of C++ did not support exception handling. Thus, standard libraries of C++ (such as the I/O library) did not use exceptions, but relied on error codes. This led to the practice of using return codes for error-checking in C++ program. Moreover, earlier compilers provided poor support for exception handling. The net result is that even now, most C++ programs do not use exceptions. In Java, exceptions were included from the beginning. All standard libraries communicate errors exclusively via exceptions. This has led to a situation where all Java programs use exception handling model for error-checking, as opposed to using return codes. As a result, Java programs crash much less frequently due to unchecked errors as compared to C++ programs. Implementation of Exception Handling in Programming Languages --------------------------------------------------------------- Exception handling can be implemented by adding "markers" to the activation records that indicate the points in the program where exception handlers are available. For instance, in C++, entering a try-block at runtime would cause such a marker to be put on the stack. When an exception arises, the runtime support code for the language searches down the stack for such a marker. (This marker may be found within the procedure that raised the exception, or it may be in a function that called it.) All ARs above this marker are popped off the stack. (This means that we free the storage for these activation records, and also reset the environment pointer etc to correspond to the try-block associated with the marker.) The exception is then "handed" to the first catch statement associated with the try-block that matches the exception. If no matching catch statement is present, then the search for a marker is contined further down the stack, and the whole process is repeated.