7. Exceptions

  1. 7.1. Try blocks
  2. 7.2. Local variables
  3. 7.3. Throwing an exception
  4. 7.4. Handling an exception
  5. 7.5. Exception specifications

Conceptually, exception handling can be described in terms of the following diagram:

current end terminate object destroy try catch
Figure 12. Try Stack

At any point in the execution of the program there is a stack of currently active try blocks and currently active local variables. A try block is pushed onto the stack as it is entered and popped from the stack when it is left (whether directly or via a jump). A local variable with a non-trivial destructor is pushed onto the stack just after its constructor has been called at the start of its scope, and popped from the stack just before its destructor is called at the end of its scope (including before jumps out of its scope). Each element of an array is considered a separate object. Each try block has an associated list of handlers. Each local variable has an associated destructor.

Provided no exception is thrown this stack grows and shrinks in a well-behaved manner as execution proceeds. When an exception is thrown an exception manager is invoked to find a matching exception handler. The exception manager proceeds to execute a loop to unwind the stack as follows. If the stack is empty then the exception cannot be caught and std::terminate is called. Otherwise the top element is popped from the stack. If this is a local variable then the associated destructor is called for the variable. If the top element is a try block then the current exception is compared in turn to each of the associated handlers. If a match is found then execution jumps to the handler body, otherwise the exception manager continues to the next element of the stack.

Note that this description is purely conceptual. There is no need for exception handling to be implemented by a stack in this way (although the default implementation uses a similar technique). It does however serve to illustrate the various stages which must exist in any implementation.

7.1. Try blocks

At the start of a try block a variable of shape:

~cpp.try.type : () -> SHAPE

is declared corresponding to the stack element for this block. This is then initialised using the token:

~cpp.try.begin : ( EXP ptb, EXP POINTER fa, EXP POINTER ca ) -> EXP TOP

where the first argument is a pointer to this variable, the second argument is the TDF current_env construct, and the third argument is the result of the TDF make_local_lv construct on the label which is used to mark the first handler associated with the block. Note that the last two arguments enable a TDF long_jump construct to be applied to transfer control to the first handler.

When control exits from a try block, whether by reaching the end of the block or jumping out of it, the block is removed from the stack using the token:

~cpp.try.end : ( EXP ptb ) -> EXP TOP

where the argument is a pointer to the try block variable.

7.2. Local variables

The technique used to add a local variable with a non-trivial destructor to the stack is similar to that used in the dynamic initialisation of global variables. A local variable of shape ~cpp.destr.type is declared at the start of the variable scope. This is initialised just after the constructor for the variable is called using the token:

~cpp.destr.local : ( EXP pd, EXP POINTER c, EXP PROC ) -> EXP TOP

where the first argument is a pointer to the variable being initialised, the second is a pointer to the local variable to be destroyed, and the third is the destructor to be called. At the end of the variable scope, just before its destructor is called, the token:

~cpp.destr.end : ( EXP pd ) -> EXP TOP

where the argument is a pointer to destructor variable, is called to remove the local variable destructor from the stack. Note that partially constructed objects are destroyed within their constructors (see §6.3) so that only completely constructed objects need to be considered.

In cases where the local variable may be conditionally initialised (for example a temporary variable in the second operand of a || operation) the local variable of shape ~cpp.destr.type is initialised to the value given by the token:

~cpp.destr.null : () -> EXP d

(normally it is left uninitialised). Before the destructor for this variable is called the value of the token:

~cpp.destr.ptr : ( EXP pd ) -> EXP POINTER c

is tested. If ~cpp.destr.local has been called for this variable then this token returns a pointer to the variable, otherwise it returns a null pointer. The token ~cpp.destr.end and the destructor are only called if this token indicates that the variable has been initialised.

7.3. Throwing an exception

When a throw expression with an argument is encountered a number of steps performed. Firstly, space is allocated to hold the exception value using the token:

~cpp.except.alloc : ( EXP VARIETY size_t ) -> EXP pv

the argument of which gives the size of the value. The space allocated is returned as an expression of type void *. Secondly, the exception value is copied into the space allocated, using a copy constructor if appropriate. Finally the exception is raised using the token:

~cpp.except.throw : ( EXP pv, EXP pti, EXP PROC ) -> EXP BOTTOM

The first argument gives the pointer to the exception value, returned by ~cpp.except.alloc, the second argument gives a pointer to the run-time type information for the exception type, and the third argument gives the destructor to be called to destroy the exception value (if any). This token sets the current exception to the given values and invokes the exception manager as above.

A throw expression without an argument results in a call to the token:

~cpp.except.rethrow : () -> EXP BOTTOM

which re-invokes the exception manager with the current exception. If there is no current exception then the implementation should call std::terminate.

7.4. Handling an exception

The exception manager proceeds to find an exception in the manner described above, unwinding the stack and calling destructors for local variables. When a try block is popped from the stack a TDF long_jump is applied to transfer control to its list of handlers. For each handler in turn it is checked whether the handler can catch the current exception. For ... handlers this is always true; for other handlers it is checked using the token:

~cpp.except.catch : ( EXP pti ) -> EXP VARIETY int

where the argument is a pointer to the run-time type information for the handler type. This token gives 1 if the exception is caught by this handler, and 0 otherwise. If the exception is not caught by the handler then the next handler is checked, until there are no more handlers associated with the try block. In this case control is passed back to the exception manager by re-throwing the current exception using ~cpp.except.rethrow.

If an exception is caught by a handler then a number of steps are performed. Firstly, if appropriate, the handler variable is initialised by copying the current exception value. A pointer to the current exception value can be obtained using the token:

~cpp.except.value : () -> EXP pv

Once this initialisation is complete the token:

~cpp.except.caught : () -> EXP TOP

is called to indicate that the exception has been caught. The handler body is then executed. When control exits from the handler, whether by reaching the end of the handler or by jumping out of it, the token:

~cpp.except.end : () -> EXP TOP

is called to indicate that the exception has been completed. Note that the implementation should call the destructor for the current exception and free the space allocated by ~cpp.except.alloc at this point. Execution then continues with the statement following the handler.

To conclude, the TDF generated for a try block and its associated list of handlers has the form:

variable (
    long_jump_access,
    stack_tag,
    make_value ( ~cpp.try.type ),
    conditional (
	handler_label,
	sequence (
	    ~cpp.try.begin (
		obtain_tag ( stack_tag ),
		current_env,
		make_local_lv ( handler_label ) ),
		try-block-body,
		~cpp.try.end ),
	    conditional (
		catch_label_1,
		sequence (
		    integer_test (
			not_equal,
			catch_label_1,
			~cpp.except.catch (
			    handler-1-typeid ) )
		    variable (
			handler_tag_1,
			handler-1-init (
			    ~cpp.except.value ),
			sequence (
			    ~cpp.except.caught,
			    handler-1-body ) )
		    ~cpp.except.end )
		conditional (
		    catch_label_2,
		    further-handlers,
		    ~cpp.except.rethrow ) ) ) )

Note that for a local variable to maintain its previous value when an exception is caught in this way it is necessary to declare it using the TDF long_jump_access construct. Any local variable which contains a try block in its scope is declared in this way.

To aid implementations in the writing of exception managers the following standard tokens are provided:

~cpp.ptr.code : () -> SHAPE POINTER ca
~cpp.ptr.frame : () -> SHAPE POINTER fa
~cpp.except.jump : ( EXP POINTER fa, EXP POINTER ca ) -> EXP BOTTOM

These give the shape of the TDF make_local_lv construct, the shape of the TDF current_env construct, and direct access to the TDF long_jump access. The exception manager in the default implementation is a function called __TCPPLUS_throw.

7.5. Exception specifications

If a function is declared with an exception specification then extra code needs to be generated in the function definition to catch any unexpected exceptions thrown by the function and to call std::unexpected . Since this is a potentially high overhead for small functions, this extra code is not generated if it can be proved that such unexpected exceptions can never be thrown (the analysis is essentially the same as that in the exception analysis check; see tdfc2pragma).

The implementation of exception specification is to enclose the entire function definition in a try block. The handler for this block uses ~cpp.except.catch to check whether the current exception can be caught by any of the types listed in the exception specification. If so the current exception is re-thrown. If none of these types catch the current exception then the token:

~cpp.except.bad : ( SIGNED_NAT ) -> EXP TOP

is called. The argument is 1 if the exception specification includes the special type std::bad_exception, and 0 otherwise. The implementation should call std::unexpected, but how any exceptions thrown during this call are to be handled depends on the value of the argument.