Although the example is using the Microsoft compiler, the principles will hopefully be of general interest.
Firstly, what was my issue?
I'm writing C++ code which uses exceptions, and using some libraries which can also throw exceptions. Unfortunately it seems to be hard to provide useful information when exceptions occur, unless you know the types of exceptions which might be thrown; and as in this case there were several, unrelated, exception hierarchies involved as well as character literals this was not easy to discover.
A common idiom in standard C++ for such cases is to have a 'generic' exception handler function which is called from a catch statement. This simply re-throws the exception within another try/catch block where a catch handler is written for each known type.
An extremely simple example of this idiom follows:
// Called from within a catch handler to log a string from the current exception
void logException()
{
std::string error;
try
{
// re-throw the exception to have another look at it
throw;
}
catch ( std::exception & ex )
{
error = ex.what();
}
catch ( CException * pEx )
{
char szMsg[255];
pEx->GetErrorMessage(szMsg, sizeof( szMsg ));
pEx->Delete();
error = szMsg;
}
// etc etc
catch ( const char *pStr )
{
error = pStr;
}
catch (...)
{
// Search me...
throw;
}
std::cerr << "Uncaught exception: " << error << std::endl;
}
// Example of use
int main( int argc, char **argv )
{
try
{
return realmain( argc, argv );
}
catch ( ... )
{
logException();
return 1;
}
}
There were two problems with this approach. The first was that the idiom does not work in my current
environment (Microsoft VC 6.0) because the destructor is called twice for re-thrown exceptions.
This bug appears to have finally gone in VC.NET, but I need to continue supporting VC 6.
The best work around for this bug is to ensure all destructors for exception objects can be
called twice, but since I didn't write all the exception objects this was not an option.
The second problem with this approach is that it only works where you know the complete list of exceptions which can be thrown. Given the lack of documentation which seems endemic in our industry it is all too easy to miss one.
What happens then depends on your runtime. The C++ standard simply says: "If no matching handler is found in a program, the function terminate() is called; whether or not the stack is unwound before this call to terminate() is implementation defined".
My environment produces the message "Abnormal program termination" which provides little help in finding the root cause of the problem.
I decided that the lack of useful information meant I was spending too much time debugging problems and so I was prepared to consider using some non-standard code to help me.
It is obvious from how the exception mechanism works that the runtime must know the type of the thrown object so it can match it to the appropriate catch handler. I decided that if I could get the runtime type of the exception many of my problems would be over.
Unfortunately this is not (yet) possible in standard C++. there have been a few discussions I have read over the years but, as far as I know, there still isn't a concrete proposal to provide this feature.
Of course at this point any Java or C# programmers reading this article are proably feeling pretty smug, since these languages (for a variety of sound reasons), provide significantly more information about exceptions than C++ does.
After some "poking around" in the compiler output and referring to some articles in MSJ by Matt Peitrek I discovered that I could write some Microsoft specific code to catch the underlying Win32 exception used to implement Microsoft's exception handling, and decode it to get hold of the type_info for the thrown exception.
So my new, highly non-portable code, looks like this:
and the corresponding sample usage is:
#include <windows.h>
#include <typeinfo>
#include <iostream>
// Microsoft specific code
DWORD logException( _EXCEPTION_POINTERS* pException )
{
// exception code for MSVC C++ exception
static const int MsvcExceptionCode( 0xe06d7363 );
// see EXSUP.INC for only public definition
static const int MagicNumber1( 0x19930520 );
PEXCEPTION_RECORD ExceptionRecord = pException->ExceptionRecord;
if ( ( ExceptionRecord->ExceptionCode == MsvcExceptionCode ) &&
( ExceptionRecord->NumberParameters == 3 ) &&
( ExceptionRecord->ExceptionInformation[0] == MagicNumber1 ) )
{
struct link
{
link *chain;
};
// Hackery to get the type_info buried in the data
link *p = (link*)ExceptionRecord->ExceptionInformation[2];
p = p[3].chain;
p = p[1].chain;
const std::type_info *t = (const std::type_info *)(p[1].chain);
std::cerr << "Uncaught exception: " << t->name() << std::endl;
return EXCEPTION_EXECUTE_HANDLER;
}
return EXCEPTION_CONTINUE_SEARCH;
}
However, I have gone from the original code which used standard C++ features to some code which uses
several Microsoft specific calls and, even worse, a number of undocumented 'magic numbers' and pointer
hacks.
#include <excpt.h>
int main( int argc, char **argv )
{
__try
{
return realmain( argc, argv );
}
__except( logException( exception_info() ) )
{
}
return 0;
}
This raises a few questions, and motivated me to write this article.
There is also a separation between code used for development and code shipped in production systems. I would be much less happy to ship a product which relied on this trick for normal operation, but if something goes wrong handling an uncaught exception I'm not really much worse off.
Encapsulating the 'trickery' is important for several reasons.
The big problem with non-portable code is maintainability. This is a problem for many reasons.
First of all I have used undocumented features of the compiler and so my chances of support if anything goes wrong is almost zero. Since I am using such features I am relying on my guesses about the structures being right. It is quite likely my guesses are incomplete, causing the code to break under some conditions.
Then I have a vulnerability to the future - I am at the mercy of the compiler writers who may well change this sort of internal detail with a subsequent release of the compiler (or even with a patch release). Will it be possible for me to change my trick to cater for the future structures? Will I still be around to do this work by then, and if not is there anyone else who could?
The best I can do to reduce this vulnerability is to ensure the function is documented to use non-portable code and provide some documentation about the 'detective work' which went in to finding this solution. A good set of test cases exercising the function in as wide a variety of cases as possible and compiled with as many versions of the compiler I can find helps greatly with ensuring the robustness of the solution and also provides a ready made test bed for future compiler releases.
Having made the function a separate compilation unit, as described above, also makes it easier to write stand-alone test cases.
I hope this quick run through the issues I thought about when writing this non-standard piece of code may help you when you find youself facing a problem which you can't seem to fix using the standard set of features.