Lecture 10: Exceptions



Lecture 10: Exceptions

10.1 goto
C++ exceptions are like C++/C long jumps which are in turn like global goto statements. The goto remember is the sole application of "function" scope in C++/C and allows the transfer of control between any two places in a function.
f() {
  ...
  label:
  ...
  goto label;
}
Very flexible, but it certainly can be abused, for example be transferring control from outside of a loop to inside it which will almost certainly cause problems. Two valid uses are:
  1. Break out of nested loops, for example:
2.  for (..;..;..)
3.    for (..;..;..)
4.      for (..;..;..)
5.        for (..;..;..)
6.        {
7.          ...
8.          if (get_out) goto xit;
9.          ...
10.      }
11.xit:
12....
This could be accomplished by defining flags and checking the flags in the control statements of the for loops, but that solution would slow things down and make the code more intricate.
  1. Exception handling, for example:
14.f()
15.{
16.  ...
17.  if (problem) { error handling setup code; goto handle_error; }
18.  ...
19.  if (problem) { error handling setup code; goto handle_error; }
20.  ...
21.  return 0; // normal return
22.  handle_error: // error handling code
23.  ...
24.  return error_code;
25.}
This allows sharing of error handling code, and doesn't complicate the flow of control too much.
Other than these applications, the traditional advice of avoiding the use of goto is sound in most cases.
8.2 longjmp()
The ANSI C long jump is accessed via the include statement
#include <setjmp.h>
which declares a type (jmp_buf) and two functions:
int setjmp(jmp_buf);
void longjmp(jmp_buf, int);
The jmp_buf type is used to store information about the current state of the run-time stack so it can be restored to that state if a long jump is executed. setjmp() initializes the buffer and returns 0. If a long jump occurs, control returns to the original setjmp() call whose return value now becomes that specified by the longjmp() call. It is a global form of goto and should be used in the same manner:
  1. Break out of nested function calls, for example:
2.  f(TNode *tree)
3.  {
4.    TNode *node;
5.    jmp_buf buf;
6.   
7.    switch(setjmp(buf))
8.    {
9.    case 0: // initiate search
10.    search(tree, &node, buf, "hello, world");
11.    break;
12.  case 1: // failed search
13.    ...
14.    break;
15.  case 2: // successful search
16.    ...
17.    break;
18.  }
19.}
20. 
21.void search(TNode *branch, TNode **found, jmp_buf buf, char *item)
22.{
23.  if (!branch) longjmp(buf, 1);
24.  else if (strcmp(branch->value, item) < 0)
25.    search(branch->left, found, buf, item);
26.  else if (strcmp(branch->value, item) > 0)
27.    search(branch->right, found, buf, item);
28.  else { *found = branch; longjmp(buf, 2); }
29.}
Again, a system of flags would be another way to handle this, but it would be slower and complicate the code.
  1. Exception handling, for example:
31.main()
32.{
33.  jmp_buf buf;
34.  switch(setjmp(buf))
35.  {
36.  case 0:
37.  main_loop:
38.    switch(request_choice(menu))
39.    {
40.      ...
41.    }
42.  case MEM_EXCEPTION:
43.    // handle non-fatal memory exception
44.    ...
45.    // re-enter loop
46.    goto main_loop;
47.  case DEVICE_EXCEPTION:
48.    // handle fatal device exception
49.    ...
50.    return code;
51.  }
52.  return 0;
53.}
Like the goto, it must be used in a very structured way, or the code will become unreadable and unmaintainable.
8.3 C++ Exceptions
In C++, exceptions are essentially a more sophisticated and flexible implementation of the long jump idea. Doing the jump is beset with more complexities for the C++ compiler implementor, since all the objects which go out of scope during the unwinding process for the run-time stack must be destroyed. However, this is of course a boon for the programmer. A long jump in the C++ context will not result in this happening.
Syntax: try block followed immediately by one or more catch blocks. try block takes place of setjmp(). Usage possibilities are approximately as with the long jump:
  1. Break out of nested function calls, for example:
2.  f(TNode *tree)
3.  {
4.    TNode *node;
5.   
6.    try {
7.      search(tree, &node, "hello, world");
8.    }
9.    catch(int return_code)
10.  {
11.    switch(return_code)
12.    {
13.    case 1: // failed search
14.      ...
15.      break;
16.    case 2: // successful search
17.      ...
18.      break;
19.    }
20.  }
21.}
22. 
23.void search(TNode *branch, TNode **found, char *item)
24.{
25.  if (!branch) throw 1;
26.  else if (strcmp(branch->value, item) < 0)
27.    search(branch->left, found, item);
28.  else if (strcmp(branch->value, item) > 0)
29.    search(branch->right, found, item);
30.  else { *found = branch; throw 2; }
31.}
  1. Exception handling, for example:
33.main()
34.{
35.  main_loop:
36.  try {
37.    switch(request_choice(menu))
38.    {
39.      ...
40.    }
41.  }
42.  catch(int exception)
43.  {
44.    switch(exception)
45.    {
46.    case MEM_EXCEPTION:
47.      // handle non-fatal memory exception
48.      ...
49.      // re-enter loop
50.      goto main_loop;
51.    case DEVICE_EXCEPTION:
52.      // handle fatal device exception
53.      ...
54.      return code;
55.    }
56.  }
57.  return 0;
58.}
The above examples mimic the long jump by throwing ints; however, any type can be thrown, and it can be caught by value or by reference. The catch statement is a bit like a one-argument function call, except only polymorphic conversions are available. Thus the first example could be rewritten as:
class FoundNode {
  TNode *m_node;
public:
  FoundNode(TNode *node) : m_node(node) { }
  TNode *node() { return m_node; }
};
 
f(TNode *tree)
{
  try {
    search(tree, "hello, world");
  }
  catch(FoundNode node)
  {
    if (node.node()) // successful search
    {
      ...
    }
    else // failed search
    {
      ...
    }
  }
}
 
void search(TNode *branch, char *item)
{
  if (!branch) throw FoundNode(0);
  else if (strcmp(branch->value, item) < 0)
    search(branch->left, item);
  else if (strcmp(branch->value, item) > 0)
    search(branch->right, item);
  else throw FoundNode(branch);
}
When an exception is detected and an object is thrown, the matching catch block (or it can match a base class of object being caught) with the most closely nested try block handles the exception. When there are two or more matching catches for the closest try, the first catch after the try block is used.
If the catch handler wishes to look at the object, it has to give it a name in the header of the catch block.
catch(...) catches anything.
A catch block can "re-throw" exception to more outer catch blocks by just saying throw without specifying an object. Could the re-throw possibly land in a later catch for the same try block?
If no catch block is found for the exception, the function terminate() is called. The function
PFV set_terminate(PFV)
(typedef void (*PFV)() applies here) can be used to install a new function for terminate() to call. By default it calls abort(). Any function installed by set_terminate() should not return.
Functions can declare the exceptions they will allow to be thrown by them or the functions they use. By default, any exception can be thrown.
If a function throws an exception which isn't in its list of declared exceptions, the function unexpected() is called. PFV set_unexpected(PFV) can be used to tailor this. By default unexpected() calls terminate().
There are three types of code that exceptions can create problems for (The following notes draw much from Margaret Ellis and Martin Carroll, "Tradeoffs of Exceptions", C++ Report, Vol. 7, No. 3 (March-April 1995), pp. 12-16.):
  1. Doing something (e.g. heap memory acquisition, handler function installations, changing stream's or other object's state) which is to be undone by later code. Problems arise when exception occurs after something has been done, but before it is undone.
This problem is fixed by making sure all these operations are carried out within the context of construction and destruction of an object. Some "weightless" code can be added to achieve this. For example, instead of coding a function as follows:
void f()
{
  PFV *old_new_handler = set_new_handler(my_new_handler);
 
  ... // this code may generate an exception
 
  set_new_handler(old_new_handler);
}
It would be preferable, when the possibility exists that an exception might be thrown during the course of the function's execution, to code it as follows:
class install_new_handler {
  PFV *m_old_new_handler;
public:
  install_new_handler(PVF *new_handler) {
    m_old_new_handler(set_new_handler(new_handler));
  }
   install_new_handler() {
    set_new_handler(m_old_new_handler)
  }
};
 
void f()
{
  install_new_handler(my_new_handler);
 
  ... // this code may generate an exception
}
Even if exceptions are not a problem, this technique has a lot to recommend it since it makes sure whatever needs undoing gets undone without the programmer having to remember to write the appropriate code.
  1. Exception gets thrown while inside a constructor. C++ exception handlers don't call the destructor for this object, so object must make sure any partial allocations get undone.
This can be done using a try, catch and re-throw combination where the universal catch block takes care of undoing that which must be undone.
stack::stack(int sz)
{
  try {
    val = new char[size = size];
    notify(); // possibly generates exception
  }
  catch(...)
  {
    delete[] val;
    throw;
  }
  top = val;
}
  1. Exception gets thrown while inside a member function for a class while the object is in an invalid state.
Same solution as in 2 works here.
Exception techniques for templates. Example stack underflow or overflow. In the case of overflow, the exception report could contain the value of the object being pushed. Exception classes can be templated and inherit from common base class. This allows application to handle stack exceptions in a general or specific way.



0 comments: