Copyright © 1994-1995 by Vladimir Belkin .
(a translation of my article from PC MAGAZINE/Russian Edition #4 1995, page 180)
E-mail: koyaanisqatsi@narod.ru

C++ exception handling: what, when, where.

    Exception handling facilities, described in "The Annotated C++ Reference Manual. Margaret A. Ellis, Bjarne Stroustrup" (ARM), is an important part of the language. The use of these facilities improves a program structure. Modern C++ compilers have native implementation of C++ exception facilities, but there are some reasons to implement these facilities on the library level. The main reason for this implementation is that the C++ compilers without native support of exceptions like Borland C++ 3.1, Symantec C++ 6.xx or MSVC 1.x are still in use. Also, native exception handling produces valuable runtime overhead, sometimes this overhead can be reduced when the exception handling facilities are implemented in the C++ program level.

     There are two reasons why I don't use the 'emulation'. The first reason is that the described exception processing schema has some differences from the ARM schema. The second reason is that the described implementation has its own advantages.

    This exception handling implementation was designed by the author for his software "Ñharms" - a C++ library for Win16/Win32(s) software development. This implementation of C++ exceptions was tested in Symantec C++ 6.11, MSVC 1.5 and GNU C++. This implementation of exception handling is not thread safe, but it can be easy adopted for a multithread environment. To adopt it to Win32 multithread model it is necessary to use thread local storage (TLS).

   There was a Microsoft's attempt to emulate C++ exception facilities in their MFC library. This attempt can not be called successful, because the Microsoft implementation does not support stack unwinding (destructors call) and does not allow to throw exceptions of arbitrary type (int, char *).

   The described implementation supports the both facilities mentioned above. Also this implementation allows, in some cases, to use more effective coding style rather than ARM style.

    Unhappily, the described implementation does not support so valuable C++ exception handling facility as grouping: a catch for a class "T" will not intercept exceptions of classes derived from "T". This is the main weakens of this implementation.

   Introduce some considerations to describe differences between exception processing schemes.

   The difference between this implementation schema and the ARM is that    This difference means that when the explicit part of an object constructor throws an exception the ARM schema assumes that only destructors of completed subobjects will be called, but not the destructor of the entire object. Contrary, the described schema assumes that the entire object's destructor will be called in this case while the stack unwinding..

   When object's subobject constructor throws an exception, both schemes assume that only destructors of existing subobjects should be called.  Then of the differences, described above, is that the ARM schema can not be acceptably implemented on the C++ program level.   Consider that the class "A" that should be compiled by C++ compiler with native exception handling support and the class constructor can throw exceptions.
 

struct X_A { // this is an exception type
   unsigned case_num;
   X_A(unsigned case_n):case_num(case_n) {}
};
class MemBlock {
  char *p;
public:
  MemBlock(size_t sz,unsigned case_n) {
      p=new char[sz];
      if (p==NULL) throw X_A(case_n);
  }
  operator char * () const { return p; }
  ~MemBlock() { delete [] p; }
};

class A {
  MemBlock p1,p2,p3;
public:
  A():p1(100,1),p2(100,2),p3(100,3) {
    // do something
  };
  ~A() {
    // do something
  }
};

    Described in this article implementation of the C++ exceptions allows to design the class "A", that can be compiled by a compiler without native exception handling , by the similar way as you can see in the below sample.
 

struct X_A { // this is an exception type
 unsigned case_num;
  X_A(unsigned case_n):case_num(case_n) {}
};
DeclareException(X_A,101) // Bind the exception type to the index
class MemBlock COLON_CHECKED {
  // The "COLON_CHECKED" macro notices that
  // the class object's destructor will be called while
  // a stack unwinding when the class object is
  // automatically allocated.
  char *p;
public:
  MemBlock(size_t sz,unsigned case_n) {
    p=new char[sz];
    if (p==NULL) xThrow(X_A(case_n));
    // "xThrow" macro is an equivalent
    // of standard C++ "throw" keyword.
  };
  operator char * () const { return p; }
  ~MemBlock() { if (p!=NULL) delete [] p; }
};
class A COLON_CHECKED {
  MemBlock p1,p2,p3;
public:
  A():p1(100,1),p2(100,2),p3(100,3) {
     resetChecked();
     // It is necessary to call inherited
     // "resetChecked" method to avoid
     // repeated call of subobjects destructors.
  };
  ~A() {
     // do something
  }
};
    However the described in this article implementation of the exception handling allows to rewrite the class "A" by the other, probably more effective way.
 
struct X_A { // this is an exception type
   unsigned case_num;
   X_A(unsigned case_n):case_num(case_n) {}
};
DeclareException(X_A,101) // Bind the exception type to the index
class A COLON_CHECKED {
  char *p1,*p2,*p3;
public:
  A():p2(NULL),p3(NULL) {
     if ((p1=new char[100])==NULL) xThrow(X_A(1));
     if ((p2=new char[100])==NULL) xThrow(X_A(2));
     if ((p3=new char[100])==NULL) xThrow(X_A(3));
     // do something
  }
  ~A() {
    if (p1!=NULL) delete [] p1;
    if (p2!=NULL) delete [] p2;
    if (p3!=NULL) {
      delete [] p3;
      // do something
    }
  }
};
    The next two samples adjust difference between the approach described in this article and the ARM approach. The first sample written for a compiler with native implementation of exceptions.
 
class A {
 char *p;
public:
  A() {
   p=new char[100];
   if (p==NULL) throw ERR;
   try {
    // an exception can be thrown here.
   } catch() {
     delete [] p; throw;
   }
  }
  ~A() {
     delete [] p;
  }
};
    The second sample shows the equivalent class that written for my implementation of the exception handling.
 
class A COLON_CHECKED {
  char *p;
public:
   A() {
     p=new char [100];
     if (p==NULL) xThrow(ERR);
     // an exception can be thrown here
  }
  ~A() {
     if (p!=NULL) delete [] p;
  }
};
    The following part of the article describes details of the implementation. The front end of the described implementation contains the next macros, classes, methods and global functions:
  1. "xTry" macro is an equivalent of the standard C++ "try" keyword;
  2. "xTryEnd" macro terminates a try block;
  3. "xCatch(T,v)" macro is an equivalent of the standard C++ "catch(T v)" clause where "T" is the type of the exception and "v" is the variable that accepts the passed value;
  4. "xCatchType(T)" macro is an equivalent of the standard C++ clause "catch(T)";
  5. "xCatchAll" macro is an equivalent of the standard C++ clause "catch(...)";
  6. "xThow(v)" global function is an equivalent of the standard statement "throw v";
  7. "xThow()" global function is an equivalent of the standard C++ statement "throw" without any argument;
  8. Global functions "void terminate()" and "PVF set_terminate(PFV)" behave exactly so as it was defined in ARM.
  9. Classes that should participate in the stack unwinding should inherit from the class "Checked". I will use the term a "checked class" for classes that inherit from "Checked" and the term "checked object" to denote an instance of a checked class.
  10. The next macros:
  11. #define COLON_CHECKED :protected Checked
    #define CHECKED_COMMA protected Checked ,
    #define COLON_VCHECKED :protected virtual Checked
    #define VCHECKED_COMMA protected virtual Checked ,
      It is necessary to use these macros to write the portable code that could be compiled as well by modern compilers with native exception handling as by compilers without this facility.
  12. The method "void Checked::resetChecked()" that should be called when an explicit part of the checked class constructor takes the control and the instance of this class has checked subobjects.
  13. Macros "DeclareTypeId(T, id)" and "DeclareException(T, id)" bind the type "T" with unique integer identifier "id". It is necessary to call one from these macros before throwing of "T" type exception.
  14. The global function "void SetTheStackBottom(void *p)" should be called before any use of this exception handling system. It is necessary for the exception handling system to recognize when an object is automatic. The example below demonstrates use of the function.
void main(argc, ... ) {
  SetTheStackBottom(&argc);
}

   It is necessary to explore the method "void Checked::resetChecked()" use. Consider that there is a checked class "A" that has a subobject of another checked class "B". In this case "resetChecked();" should be the first statement of the class "A" constructor. This call is needed to avoid the repeated call of the class "B" subobject destructor.

  Before the exploration of the implementation background I want to show the next realistic example of the use of this exception handling system.
 

#define STRICT
// HDC and HWND are different types
// when STRICT defined
#include<windows.h>
#include "except.h"
struct X_DC {
  HWND hwnd;
  X_DC(HWND wnd):hwnd(wnd) {}
};
DecalreException(X_DC,8001)
// Constructors of classes that wrap
// MS Windows window
// graphics device contexts can throw
// exceptions of the class "X_DC".
// See the next example.
class DC: CHECKED_COMMA public Rect {
  void operator = (const DC &);
  // disable assignment
  DC(const DC &);
  // disable the copy constructor
  HDC dc;
  HWND hwnd;
public:
  DC(HWND wnd):hwnd(wnd) {
    if ((dc=GetDC(hwnd))==NULL) xThow(X_DC(hwnd));
    GetClientRect(hwnd,(RECT *)this);
  }
  operator HWND () const { return hwnd; }
  operator HDC () const { return dc; }
  ~DC() {
     if (dc!=NULL) ReleaseDC(hwnd,dc);
  }
};
// This is an example of try-block
xTry {
   DC dc1(hwnd);
   // perform some drawing
} xCatch(X_DC,except) {
   char buf[60];
   sprintf(buf,
     "can not create DC for"
     "the window %x\n", except.hwnd);
     OutputDebugString(buf);
     xThrow(); // throw the exception again
}
xTryEnd

    Now consider the implementation of the exception handling system. The system uses two stacks: the try-block stack and the checked object's stack. Both these stacks are implemented as linked lists. The last from these lists also has name "kill-list". The top of the first stack points to the current try-block, and the top of the second list points to the last created automatic object (subobject). Pointers to the tops of the stacks are stored in the global variables. These. Therefore, adoption of this exception handling system to multithread environment requires TLS.

    In the begin of a program execution both these stacks are empty. When a try-block takes the control then the system pushes this try-block to the try-block stack. When the try-block leaves (or loses) the control the system removes this try-block from the stack. Of course, the system pushes and removes not the try-blocks themselves, but instances of the special class "TryToExecute". When a try-block takes the control, it creates an instance of this class. Here you can see the declaration of this class.
 

struct TryToExecute {
  jmp_buf buf;
  Checked __ss *chk;
  TryToExecute __ss *tr;
  int type;
  void __ss *lvalue;
  void OnError(int type, void __ss *ptr);
  TryToExecute();
  ~TryToExecute();
};

   You can see below definitions of the global variables that keep the exception handling system current state.
 

Checked __ss *TheLastChecked=NULL;
// this variable points to the top
// of the "kill-list".
TryToExecute __ss *TheLastTryToExecute=NULL;
// this variable points to the top
// of the try-block stack
int TheLastExceptionType=-1;
// this variable stores an integer
// identifier of the last thrown
// exception.
// This identifier was previously
// bound to the exception type
// using "DeclareTypeId" or
// "DeclareExceptionId" macros.
void *TheLastExceptionLvalue=NULL;
// this is a pointer to a buffer
// that holds a thrown object.
void __ss *TheStackBottom=NULL;
// The bottom of the program stack
    The system uses the last variable to detect whether an object is a stack located. This implementation of exception handling uses this function to perform the detection.
 
int StackLocated(void __ss *p) {
  return
     // Intel based platforms
     // and others where stack
     // grows toward lesser
     // addresses.
    (((char __ss *)p)>unsigned((char __ss *)&p))&&
    (((char __ss *)p)<=((char __ss *)TheStackBottom))) ||
    // Motorola and other platforms
    // where stack grows
   // toward greater addresses
   (((char __ss *)p)<unsigned((char __ss *)&p))&&
   (((char __ss *)p)>=((char _ss *)TheStackBottom)));
}

  Below you can see the try-block definition macros.
 

#define xTry { TryToExecute _TTE_; \
if (!setjmp(__TTE__.buf)) {
#define xCatch(t,val) \
} else if (typeTypeId(t)==_TTE_.type) { \
t &val=*(t *)(_TTE_.lvalue); _TTE_.type=0;
#define xCatchAll } else { _TTE_.type=0;
#define XCatchType(t) \
} else if (typeTypeId(t)==_TTE_.type) { \
_TTE_.type=0;
#define xTryEnd }}

   "TryToExecute" constructor saves the current kill-list and assigns NULL to the global variable that points to the top of the list. Destructor of this class restores the value of this saved variable and if an exception has been happened this destructor throws it again in the outer context.
To determine whether exists a thrown exception that has not been handled in the current context  the "TryToExecute" destructor checks the "type" member variable. When a catch intercepts an exception it clears this variable. Therefore if "TryToExecute" destructor see that this members is not zero then this means that there is an exception that has not been handled in the current try-block.
   When the C++ compiler supports templates it is possible to use this definition of function "xTheow".
 

template<class T> void xThrow(T value)
{ static T x;
  x=value;
  OnException(typeId(value),&x);
}

    Also in this case it is enough to use "DeclareTypeId" macro to bind integer index to type. Some archaic C++ does not support template functions. It requires the use of macro "DeclareException" instead the above mentioned macro.
 

#define DeclareException(T,Id) \
DeclareTypeId(T,Id) \
inline void xThrow(const T &value) \
{ checkExceptionSize(sizeof(T),Id); \
  new(exceptionBuffer) T(value); \
  OnException(Id,exceptionBuffer); \
}

   This requires additional global array "exceptionBuffer". The function "checkExceptionSize" checks size of the thrown object and throws another exception when the thrown object's size exceeds the global array size.
 

char exceptionBuffer[MAX_EXCEPTION_SIZE];
void checkExceptionSize(int sz,int tid) {
  if (sz>MAX_EXCEPTION_SIZE) xThrow(XX_invalid_size(tid));
}

    You can see below the global functions and methods that perform the stack unwinding, thrown object transfer and pass the control to the catches.
 

void OnException(int type,void *p) {
  TheLastExceptionType=type;
  TheLastExceptionLvalue=p;
  if (TheLastTryToExecute==NULL) {
   if (TheLastChecked!=NULL) {
      Checked *p=TheLastChecked;
      TheLastChecked=TheLastChecked->prev;
      p->DestroyChecked();
   }
   terminate();
  }
  TheLastTryToExecute->OnError(type,p);
}
void Checked::DestroyChecked() {
  this->~Checked();
  if (TheLastChecked!=NULL) TheLastChecked->DestroyChecked();
}
Checked::~Checked() {
#if M_I86LM || M_I86CM || M_I86VM
  if (FPSEG(this)==getSS())
#endif
  if (StackLocated((void *)this))
    if (TheLastChecked==this) TheLastChecked=prev;
    else
     for (Checked *p=TheLastChecked;p!=NULL;p=p->prev)
         if (p->prev==this) { p->prev=prev; break; }
}

    Here you can see the definitions of the macros "DeclareTypeId" and "typeTypeId" and the inline global overload functions "typeId" and "ptypeId".
 

#define DeclareTypeId(type,xx) \
inline int typeId(const type &) {return (xx);} \
inline int ptypeId(const type*) {return (xx);}
#define typeTypeId(T) ptypeId((T *)NULL)

  This implementation of the exception handling contains the #include file "typeid.h" where are bindings of simple C++ types like "int" and pointers to these types like "int***".
 

DeclareTypeId(short,1)
DeclareTypeId(unsigned short,2)
// ..................
DeclareTypeId(unsigned long ***,42)
DeclareTypeId(double ***,62)
DeclareTypeId(char ***,72)

   Here you can see the class "Checked" declaration.
 

class Checked {
  friend class TryToExecute;
  friend void OnException(int type,void *p);
  Checked __ss *prev;
  // 'prev' points to the previous checked
  // on the stack.
public:
  Checked &operator = (const Checked &) {return * this;}
  // this operator avoids the default assignment
  // operator effect
  Checked(const Checked &) {
    new(this) Checked();
    // Call the simple constructor
    // instead the copy constructor.
    // Use "placement new" to perform
    // this call.
  }
  void DestroyChecked();
    // If an exception has been thrown from
    // a try-block and this object is automatic
    // then this method calls
    // the object's virtual destructor and the
    // destructors of other
    // automatically allocated checked
    // objects, from the current
    // try-block kill-list.
    // Otherwise if an exception has
    // been thrown out of any try-block
    // then the methods calls destructors
    // of all existing automatically
    // allocated checked objects.
    virtual ~Checked();
    void resetChecked();
   // If the checked object contains
   // checked subobjects then
   // the object's constructor first
   // statement should be a call of
   // this method.
};

   The class "Checked" constructor recognizes whether the constructed object is an automatic object or not, and inserts this object to the kill-list only in the first case.
 

Checked::~Checked() {
#if M_I86LM || M_I86CM || M_I86VM
  if (FPSEG(this)==getSS())
#endif
  if (StackLocated((void *)this))
    if (TheLastChecked==this) TheLastChecked=prev;
       else
         for (Checked *p=TheLastChecked;p!=NULL;p=p->prev)
            if (p->prev==this) { p->prev=prev; break; }
}

Other parts of this exception handling system are trivial an any good C++ programmer can implement them.

Сайт управляется системой uCoz