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.
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; }
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:
#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.
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.