[net.lang.c++] Exception handling in C++

keith@cecil.UUCP (keith gorlen) (02/25/86)

C++ is a terrific language, but I was a bit disappointed initially that
it included no facilities for exception handling -- but wait -- maybe
"you can program that yourself"!

My favorite exception handling mechanism is the SIGNAL-ENABLE construct
of BLISS-11.  Its reasonably efficient and flexible, and it seems to
have inspired Ada's RAISE-EXCEPTION construct.

A similar facility for C++ works as follows:

	BEGINX		// start of exception block

	statements that might cause an exception to be raised

	EXCEPTION

	exception handlers; format same as body of switch statement

	ENDX		// end of exception block

As a trivial example:

	#include <stream.h>
	#include "exception.h"
	const int exception1 = 1;
	void f() { RAISE(exception1); }
	main()
	{
		BEGINX
			f();
			cerr << "This will never happen!\n";
		EXCEPTION
			case exception1: cerr << "exception1 handled\n";
		ENDX
		cerr << "End of program\n";
	}

will output:

	exception1 handled
	End of program

Of course, if no exception is raised in the first part of the block, the
handlers following the EXCEPTION keyword are not executed.  When an
exception is raised, the case with the same exception code (if any) is
executed.  Unless the flow of control is altered by a return, break,
continue, etc., execution continues with the first statement after the
ENDX whether or not an exception occurred, and whether or not a matching
case was found.

BEGINX...ENDX blocks may be nested within either part of a BEGINX...ENDX
block; i.e., the exception handlers may contain exception blocks.  An
exception is raised by executing the statement RAISE(code), causing
control to pass to the most recently executed exception block.  An
exception handler may reference the current exception code by the name
EXCEPTION_CODE.  If no case matches the current exception code, it can
be propagated up to the next most recently executed exception block by
including the statement:

	default:RAISE(EXCEPTION_CODE);

in the exception block.

The shar file attached to the end of this article contains my
implementation of this and a test program.  I would like to hear
people's comments on the following points:

1.  Will this implementation work in general, or have I forgotten
something?  Are there some dire consequences of using setjmp/longjmp in
this fashion?  Note that destructors will not be called when a block is
exited by doing a RAISE.

2.  In Ada, if an EXCEPTION block has no handler for the raised
exception, the exception is automatically propagated up to the next
level.  In my implementation, control goes to the statement after the
ENDX.  I think Ada's way is better.  Can anyone think of a way to do
this while retaining the capability of having a default error handling
case?  The problem is that there can only be one default label in a
switch statement.

2.  In Ada, the EXCEPTION block is within the scope of variables declared
in the BEGIN...EXCEPTION part.  This seems like a good idea, because the
exception handlers may want to examine these variables to decide how to handle
the exception.  It doesn't seem possible to do this directly in C++, and
it is easy to achieve the same effect simply by writing, for example:

	{
		int x;
		BEGINX
			// do something to x
			// do something that might raise an error
		EXCEPTION
			case <something>:	// use x
		ENDX
	}

But this raises the question of what happens if x is allocated to a
register.  And is there a way to force stack allocation of an auto
variable in C++?

---
	Keith Gorlen
	Computer Systems Laboratory
	Division of Computer Research and Technology
	National Institutes of Health
	Bethesda, MD 20892
	phone:	(301) 496-5363
	uucp:	{decvax!}seismo!elsie!cecil!keith

#! /bin/sh
# This is a shell archive, meaning:
# 1. Remove everything above the #! /bin/sh line.
# 2. Save the resulting text in a file.
# 3. Execute the file with /bin/sh (not csh) to create the files:
#	exception.h
#	exception.c
#	xtest.c
# This archive created: Tue Feb 25 09:14:38 1986
export PATH; PATH=/bin:$PATH
echo shar: extracting "'exception.h'" '(1810 characters)'
if test -f 'exception.h'
then
	echo shar: will not over-write existing file "'exception.h'"
else
sed 's/^	X//' << \SHAR_EOF > 'exception.h'
	X/* exception.h -- Ada -like exception handler
	X
	XAuthor:
	X	K. E. Gorlen
	X	Bg. 12A, Rm. 2017
	X	Computer Systems Laboratory
	X	Division of Computer Research and Technology
	X	National Institutes of Health
	X	Bethesda, Maryland 20892
	X	Phone: (301) 496-5363
	X	uucp: {decvax!}seismo!elsie!cecil!keith
	X	February, 1986
	X
	XFunction:
	X	
	XDeclarations for Ada -like exception handling.
	X
	XModification History:
	X	
	X*/
	X
	X#ifndef EXCEPTIONH
	X#define EXCEPTIONH
	X
	X#include <setjmp.h>
	X
	Xclass ExceptionEnv;
	Xextern ExceptionEnv* exception_env_stack_top;
	X
	Xclass ExceptionEnv {
	X	ExceptionEnv* prev;
	X	int exceptionCode;
	X	jmp_buf env;
	Xpublic:
	X	ExceptionEnv() {	// MUST be inline
	X		prev = exception_env_stack_top;
	X		exception_env_stack_top = this;
	X		exceptionCode = setjmp(env);
	X	}
	X	~ExceptionEnv() { if (exception_env_stack_top == this) pop(); }
	X	int code()	{ return exceptionCode; }
	X	void pop()	{ exception_env_stack_top = prev; }
	X	void raise(int exception);
	X};
	X
	X#define	EXCEPTION_CODE	exception_environment.code()
	X
	X#define BEGINX {							\
	X	ExceptionEnv exception_environment;				\
	X	if (EXCEPTION_CODE == 0) {					\
	X		
	X// Statements in the scope of this exception handler block go here.
	X
	X#define EXCEPTION							\
	X	}								\
	X	else switch(EXCEPTION_CODE) {					\
	X
	X/*
	XException handlers go here; the syntax is that of a switch statement
	Xbody.  The exception code that caused this EXCEPTION block to be entered
	Xmay be accessed via the macro EXCEPTION_CODE.  The statement
	X"default:RAISE(EXCEPTION_CODE);" will propagate the current exception up
	Xto the next exception handler block if the exception is not handled by
	Xthis block; otherwise, execution continues with the first statement
	Xafter this exception block.
	X*/
	X
	X#define ENDX								\
	X	};								\
	X}									\
	X
	Xinline void RAISE(int exception)
	X{
	X	exception_env_stack_top->raise(exception);
	X}
	X
	X#endif
SHAR_EOF
if test 1810 -ne "`wc -c < 'exception.h'`"
then
	echo shar: error transmitting "'exception.h'" '(should have been 1810 characters)'
fi
fi # end of overwriting check
echo shar: extracting "'exception.c'" '(838 characters)'
if test -f 'exception.c'
then
	echo shar: will not over-write existing file "'exception.c'"
else
sed 's/^	X//' << \SHAR_EOF > 'exception.c'
	X/* exception.c -- Ada -like exception handler library routines
	X
	XAuthor:
	X	K. E. Gorlen
	X	Bg. 12A, Rm. 2017
	X	Computer Systems Laboratory
	X	Division of Computer Research and Technology
	X	National Institutes of Health
	X	Bethesda, Maryland 20892
	X	Phone: (301) 496-5363
	X	uucp: {decvax!}seismo!elsie!cecil!keith
	X	February, 1986
	X
	XFunction:
	X
	XRun-time support for Ada -like exception handling.
	X	
	XModification History:
	X	
	X*/
	X#include <stream.h>
	X#include <osfcn.h>
	X#include <libc.h>
	X#include "exception.h"
	X
	XExceptionEnv* exception_env_stack_top;
	XExceptionEnv lastResort;
	X
	Xvoid ExceptionEnv::raise(int exception)
	X{
	X	if (exception == 0) {
	X		cerr << "Tried to RAISE exception code 0\n";
	X		abort();
	X	}
	X	if (prev == 0) {	// i.e.,  this == &lastResort
	X		cerr << "Unhandled exception code " << exception << "\n";
	X		exit(1);
	X	}
	X	pop();
	X	longjmp(env,exception);
	X}
SHAR_EOF
if test 838 -ne "`wc -c < 'exception.c'`"
then
	echo shar: error transmitting "'exception.c'" '(should have been 838 characters)'
fi
fi # end of overwriting check
echo shar: extracting "'xtest.c'" '(1298 characters)'
if test -f 'xtest.c'
then
	echo shar: will not over-write existing file "'xtest.c'"
else
sed 's/^	X//' << \SHAR_EOF > 'xtest.c'
	X#include "exception.h"
	X#include <stream.h>
	X
	Xenum exceptionCode { EXCEPTION1=1, EXCEPTION2, EXCEPTION3, EXCEPTION4 };
	X
	Xvoid x(exceptionCode n)
	X{
	X	BEGINX
	X		if (n>EXCEPTION2) {
	X			cerr << "Raising EXCEPTION" << n << "...";
	X			RAISE(n);
	X		}
	X		cerr << "Trying normal return from function\n";
	X		return;
	X	EXCEPTION
	X		case EXCEPTION3: cerr << "EXCEPTION3 handled\n"; return;
	X		default: cerr << "trying RAISE(EXCEPTION_CODE)...";
	X			RAISE(EXCEPTION_CODE);
	X	ENDX
	X}
	X
	Xmain()
	X{
	X	cerr << "Begin exception handler test\n";
	X
	X	BEGINX
	X		cerr << "Testing normal execution\n";
	X	EXCEPTION
	X		default: cerr << "This should not happen!\n";
	X	ENDX
	X
	X	BEGINX
	X		cerr << "Raising EXCEPTION1...";
	X		RAISE(EXCEPTION1);
	X		cerr << "EXCEPTION1 not handled!\n";
	X	EXCEPTION
	X		case EXCEPTION1: cerr << "EXCEPTION1 handled\n";
	X	ENDX
	X	
	X	BEGINX
	X		cerr << "Testing nested exception block\n";
	X		x(EXCEPTION2);
	X		BEGINX
	X			cerr << "Raising EXCEPTION2...";
	X			RAISE(EXCEPTION2);
	X			cerr << "EXCEPTION2 not handled!\n";
	X		EXCEPTION
	X			case EXCEPTION2: cerr << "EXCEPTION2 handled\n";
	X				cerr << "Raising EXCEPTION3...";
	X				x(EXCEPTION3);
	X		ENDX
	X		
	X		cerr << "Raising EXCEPTION4...";
	X		x(EXCEPTION4);
	X		cerr << "EXCEPTION4 not handled!\n";
	X
	X	EXCEPTION
	X		default: cerr << "Test unhandled exception handler\n";
	X			RAISE(EXCEPTION_CODE);
	X	ENDX
	X}
SHAR_EOF
if test 1298 -ne "`wc -c < 'xtest.c'`"
then
	echo shar: error transmitting "'xtest.c'" '(should have been 1298 characters)'
fi
fi # end of overwriting check
#	End of shell archive
exit 0

-- 
---
	Keith Gorlen
	Computer Systems Laboratory
	Division of Computer Research and Technology
	National Institutes of Health
	Bethesda, MD 20892
	phone:	(301) 496-5363
	uucp:	{decvax!}seismo!elsie!cecil!keith

rose@think.ARPA (John Rose) (02/27/86)

(Note:  I can't tell if I'm getting onto the net.  No one commented on
my previous posting on lambda expressions and expression-valued blocks in C
(net.lang.c), so I couldn't tell if I'd gotten through.  If there is
no comment on this paper too, could some kind moderator-type please
e-mail me a pat on the back saying it was at least broadcasted?)

> 1.  Will this implementation work in general, or have I forgotten
> something?  Are there some dire consequences of using setjmp/longjmp in
> this fashion?  Note that destructors will not be called when a block is
		 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
> exited by doing a RAISE.

This is a bug in longjmp() which is easily fixed with a little conspiracy
with the C++ compiler.  A guarantee of de-initialization which only applies
to local exits is not much of a guarantee.  For example, I would like to
write an open-file abstraction which ensures a close at the end of its use.
I use a stdio FILE instead of a stream to make explicit the resource management:

	class open_file {
		FILE*	fp;
	public:
		open_file(char *name, char *mode = "r")
			{ fp = fopen(name, mode); }
		~open_file()
			{ fclose(fp); }
		operator FILE*()
			{ return fp; }
		};

	void load_file(char *name)
	{	open_file in(name);
		// do something with the stream, e.g.:
		extern lispval read(FILE *), eval(lispval);
		lispval l;
		while (l = read(in))  (void)eval(l);
		// ...and when open_file goes out of scope, the right things happen
	}

Implementationally, an "open_file" is just a FILE pointer, with guaranteed
de-initialization tacked on.  The guarantee should have the force of a Lisp
UNWIND-PROTECT, or else the open_file abstraction is much less usable.
And if we don't have a garbage collector to manage storage behind our backs,
free store management requires such guarantees also.

(What other examples of guaranteed deinitialization are there?  Do they
require UNWIND-PROTECTs?  And is the use of non-local exits limited to
a few types of application, such as user interfaces and language interpreters?
Can you give an example of an application where non-local exits are inconceivable,
or where we don't care about non-local deinitialization?)

Here is some C code which would be a plausible translation of the above,
including UNWIND-PROTECTs.  You may have seen this technique in Betz's
XLISP, which is written in C.

	/* #include <longjmp.h>: */
	struct catch {
		void (*catch_fn)();
		void *catch_arg;
		struct catch *catch_prev;
		};
	extern struct catch _thread;
	/* end include */

	struct open_file {
		FILE*	fp;
		};

	void deinit_open_file(this)  void *this;
	{	fclose(((struct open_file *)this)->fp);  }

	void load_file(name) char *name;
	{	struct open_file in;
		extern lispval read(), eval();
		lispval l;
		// Establish a catch block:
		struct catch deinit_in;
		deinit_in.catch_fn = deinit_open_file;
		deinit_in.catch_arg = (void *)&in;
		deinit_in.catch_prev = _thread;
		_thread = &deinit_in;
		in.fp = fopen(name, "r");
		while (l = read(in.fp))  (void)eval(l);
		_thread = _thread->catch_prev;
		fclose(in.fp);		// inline exp. of deinit_open_file
	}

Clearly, there is an expense in establishing the catch block:  Four
extra accesses to a new structure on the stack, and two accesses to an
external variable are needed for each creation/deletion of a protected
object; an out-of-line delete function must also be compiled.  But this
expense is not incurred for classes without guaranteed deinitialization,
and perhaps it is reasonable to declare that any class which needs guaranteed
deinit. should need it consistently for non-local exits.  Also, particular
architectures such as the Vax allow cheap establishment of unwind handlers
(one write to the stack frame).

A compromise:  After the signature is an outline of a library class which
provides unwind protection, and some applications, including file closing.
The idea is that objects of class "catch" are ALWAYS deinitialized, even
by longjmp(), and so if you include such an object as one of your members,
or as a local variable, you can get a hook on stack unwinds.

----------------------------------------------------------
John R. Rose, Thinking Machines Corporation, Cambridge, MA
245 First St., Cambridge, MA  02142  (617) 876-1111 X270
rose@think.arpa				ihnp4!think!rose


	class catch {
		static catch* thread = 0;
		catch* prev;
		virtual void spring()
			{ /* your code here */ }
		friend void _unwind_catches(void *);
	public:
		catch()
			{ prev = thread; thread = this; auto_check(this); }
		~catch()
			{ thread = prev; spring(); }
		};
	void _unwind_catches(void *upto) // Normally only called on 
	{	for (catch *cur; (void *)(cur = thread) < upto; ) {
			thread = cur->prev; // same as ~catch().
			cur->spring();
			}
	}
	/* Just before longjmp unwinds each stack frame,
	 * it calls _unwind_catches() with the address of that frame.
	 */

	class generic_catch : catch {
		void (*catch_fn)();
		void *catch_arg;
		void spring()
			{ catch_fn(catch_arg); }
	public:
		generic_catch(void (*fn)(), void *arg = 0)
			{ catch_fn = fn; catch_arg = arg; }
		}

	class catch_restart : catch {
		jmp_buf from_the_top;	// HACK!  To jump out of the unwind.
		inline catch_restart()
			{ flag = setjmp(from_the_top); }
		virtual int restart_p()
			{ return 1; } // Predicate for when to restart.
		void spring()
			{ if (restart_p()) longjmp(from_the_top, 1); }
	public:
		int flag;
		}

	// Here is class open_file:
	class open_file {
		friend file_catch;
		class file_catch : catch {
			void spring() // depends on being 1st in struct:
				{ fclose(((open_file *)this)->fp); }
			} cb;
		FILE*	fp;
	public:
		open_file(char *name, char *mode = "r");
		// No destructor:  handled by member cb.
		};



-- 
----------------------------------------------------------
John R. Rose, Thinking Machines Corporation, Cambridge, MA
245 First St., Cambridge, MA  02142  (617) 876-1111 X270
rose@think.arpa				ihnp4!think!rose

keith@cecil.UUCP (keith gorlen) (03/02/86)

A couple of comments/questions on John Rose's technique described in
<4431@think.ARPA> for guaranteeing de-initialization of objects in C++
when longjmp is used:

1)  To use the example of class open_file, what if one writes

	{
		open_file f1("foo");
		open_file f2("bar");
		...
	}

and the block is exited normally; i.e., not by means of longjmp.  Does (should)
C++ guarantee that the destructors will be called in the opposite order that 
the constructors were called?  If not, the catch frame list might be corrupted.

2)  There is also a problem if one does
	
	open_file* f1 = new open_file("foo");

The catch frame constructor should probably check for this==0 before linking
the frame to the catch frame list.

3)  Please explain why spring() must be first in struct:

	class open_file {
		friend file_catch;
		class file_catch : catch {
==>			void spring() // depends on being 1st in struct:
				{ fclose(((open_file *)this)->fp); }
			} cb;
		FILE*	fp;

I think this is a very useful concept.  One could even define useful
classes whose functions are accomplished solely by
constructor/destructor functions; for example, a class CriticalRegion
might be defined with a constructor that did a WAIT on a specified
semaphore and a (guaranteed) destructor that did a SIGNAL on the same
semaphore.  Declaring a CriticalRegion object at the beginning of a
block would then assure that the semaphore was SIGNALled no matter how
the block were exited:

	{
		CriticalRegion foo(some_semaphore);
		...
	}

-- 
---
	Keith Gorlen
	Computer Systems Laboratory
	Division of Computer Research and Technology
	National Institutes of Health
	Bethesda, MD 20892
	phone:	(301) 496-5363
	uucp:	{decvax!}seismo!elsie!cecil!keith

rose@think.ARPA (John Rose) (03/03/86)

In article <56@cecil.UUCP> keith@cecil.UUCP (keith gorlen) writes:
>A couple of comments/questions on guaranteeing de-initialization of objects in C++
>when longjmp is used:
>1)  To use the example of class open_file, what if one writes
>	{	open_file f1("foo");
>		open_file f2("bar");  ...  }
>and the block is exited normally; i.e., not by means of longjmp.  Does (should)
>C++ guarantee that the destructors will be called in the opposite order that 
>the constructors were called?
"Does":  I can only find such a guarantee for static objects (C++ book, p. 158).
"Should":  Since a declaration/initialiation is guaranteed to be executed after
textually preceding decls, it may in general depend on the consistency
those preceding objects.  It would be a mistake to allow destruction
of those objects before the object which relies on them is destroyed.
In every other case in C++, initialization and destruction nest properly
(statics, base/derived orderings; member object ordering "undefined").
Bjarne, are you silent about auto objects to allow implementations
room for some efficiency hack?

>2)  There is also a problem if one does
>	open_file* f1 = new open_file("foo");
Yes, allocating catch blocks on the heap has little meaning at best.
As you say, they should not be added to the dynamic context.
>3)  Please explain why spring() must be first in struct:
>	class open_file {
>		friend file_catch;
>		class file_catch : catch {
>==>			void spring() // depends on being 1st in struct:
>				{ fclose(((open_file *)this)->fp); }
>			} cb;
>		FILE*	fp;
Oops, an unclear comment.  The "file_catch" object must be the
first member fo the "open_file" object, because of the cast
hackery by which the self-pointer of the member object is converted
to the self-pointer of the parent object.

>	{ CriticalRegion foo(some_semaphore); ... }
Lovely.  And why not allow "foo" to be omitted?  (Well, that's already
defined, and has the wrong scoping.)  Now, the only thing left to ensure
is that the block can have multiple exits.  Have you seen Unix* code like this?
	for (;;) {
		int s = spl6();
		// Critical section...
		if (time_to_break()) {
			splx(s);
			break;
			}
		// More stuff.
		splx(s);
		// Do something noncritical.
		}
(E.g., the timeout routine in 4.2.)  The dual cleanups are an eyesore,
confusing, and bug-prone.  How about:
	for (;;) {	{
			CriticalRegion access_timer_queue(6);
			// Critical section...
			if (time_to_break())  break;
			// More stuff.
			}
		// Do something noncritical.
		}
The C++ spec currently disallows the dual construction:  Multiple block
entries, and the book doesn't say anything about multiple exits either.
Since the two constructions are equivalent in implementation difficulty
(both require either a switch or duplication of code), I suggest that
both be allowed.

Imagine how clean a kernel could be if it were written well in C++.
(Or C for that matter :-}.)

-- 
----------------------------------------------------------
John R. Rose, Thinking Machines Corporation, Cambridge, MA
245 First St., Cambridge, MA  02142  (617) 876-1111 X270
rose@think.arpa				ihnp4!think!rose