Browse in : |
All
> Topics
> Programming
All > Journals > CVu > 156 Any of these categories - All of these categories |
Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.
Title: Maintaining Context for Exceptions (Alternative)
Author: Administrator
Date: 03 December 2003 13:16:02 +00:00 or Wed, 03 December 2003 13:16:02 +00:00
Summary:
Body:
At ACCU 2002 Andrei Alexandrescu talked about storing contextual information in the event of an exception being thrown. The individual elements of context mirror the unwinding of the stack and the progress from lower level code to wherever the exception finally stops and is communicated to the user.
It is at the lower level that things (normally) go wrong because that's where the work is done. Unfortunately at that level we are in often the worst position to compose a meaningful error message. The low level code lacks knowledge of the overall intent of the code - this is because we needed to break things down and wanted reuse!
We are interested in what went wrong and what we were trying to do at the time. At the point of the error we know what went wrong, and as we unwind the call stack we move through the levels of detail about what was being attempted.
If, when an error occurs we use an exception to report the specific problem and as that exception unwinds the stack we record the context, we can get a full report on what went wrong. This is helpful to the user and anyone supporting the product (who are helpful to the user).
Rob Hughes' article in the August CVu explained a system that he had developed and used in his projects. At Chersoft we were also inspired by Andrei to create a parallel system which we have successfully used in commercial 'shrink wrap' software.
In this article I'll outline the similarities and differences between our approaches, some extra bits we do and the evolution of our system - some of which has come from looking at Rob's August piece. It might be worth getting hold of it and reading it before tackling this one.
Both systems use a class for each piece of context and macros to put instances of it onto the stack (singleton pattern). We have not needed to do a 'thread specific singleton' though we considered it.
We have both 'user' and 'technical' messages - we can put all sorts of useful stuff in the technical messages that might scare the users but would be handy for programmers supporting a product.
Rob reports that std::uncaught_exception() always returns false with Borland C++ builder. Same for VC++ 6.0 - in the Dinkum STL you can read code a bit like:
bool std::uncaught_exception() { return false; }
It doesn't even try! We tried writing our own but it was desperate and we retreated.
A properly service packed VC++.NET works correctly. We develop our code to compile and run using both VC++ 6.0 and VC++.NET, so we have a workaround - our own exception base class. CExceptionBase, as it's known, increments a static int on construction and decrements it on destruction.
static bool CExceptionBase::Uncaught() { #if _MSC_VER >= 1300 return std::uncaught_exception(); #else return s_nInstances > 0; #endif }
The context class uses the above static method to determine whether an exception is active. As long as classes derived from CExceptionBase are just thrown as exceptions it is fine.
We derive various exceptions from this base and have a dialog which reports on the exception and the context stack.
So we are ok if we throw our own exceptions.
But we are at risk from STL exceptions and platform specifically:
-
Various MFC exceptions
-
COM exceptions
-
Win32 structured exception handling exceptions!
So in various ways we convert these exceptions into our own.
catch(CFileException * pEx) { throw CExceptionMFC_File(pEx); } catch(CArchiveException * pEx) { throw CExceptionMFC_Archive(pEx); } catch(CException * pEx) { throw CExceptionMFC(pEx); } catch(std::exception& ex) { throw CExceptionSTL(ex); } catch(const _com_error& ex) { throw CExceptionCOM(ex); } catch(csx::CExceptionBase&) { throw; }
This deals with the nasty way MFC news exceptions and has them deleted and so on. The information contained in the exceptions is sucked out, put in one of ours and thrown.
We put this in a macro and called it CATCH_RETHROW. The translation is handy, we can still use our dialog:
try { try { CONTEXT("Doing the work!"); // do the work involving lots of // method calls etc.. } CATCH_RETHROW; } catch(const CExceptionBase& ex) { CexceptionDlg dlg(ex, CexceptionContextStack ::Instance().GetAndResetContext()); dlg.DoModal(); }
Wonderful! But on a compiler without a proper std::uncaught_exception we miss out on an awful lot of contexts. We only get context information after CATCH_RETHROW has been done. We need more of them!
try..catch blocks are costly, the compiler needs to put up all sorts of scaffolding to support their operation. We don't want every function to have one.
As an aside... at a previous company I worked an a system where (nearly) every function was something like...
void foo() { try { // work } catch(Cexception& ex) { ex.AddMessage("Bit of context"); throw ex; } }
It was more complicated and macros were used. The code was slightly obscured, larger and slower. On the other hand it could produce good errors and there was a consistent application wide error handling scheme.
Getting back to the present...
We tend to put the extra CATCH_RETHROW blocks around where we think an exception might emerge - looking at the list of exceptions that's not too hard to do. When we switch to the VC++ .NET compiler completely the problem goes away (and it is a better compiler).
Rob's solution has a context class that pushes information onto a stack on construction and removes it if an exception is not active. Our context class pushes on destruction. If an exception were thrown during our pushing there would be two exceptions on the go at once and the application would terminate because that's not allowed.
Rob's code is exception safe as the STL deque guarantees that pop_back() will not throw.
We take a risk that on Windows memory allocation doesn't tend to fail - it just takes a very long time when memory is low. By this stage everything has gone horribly wrong anyway.
In return for taking the risk we gain quite a lot of efficiency because the context class does very little, excepting the exceptional case.
We convert Win32 structured exceptions like 'Access Violation' into C++ exceptions derived from our exception base class. This is done by hooking in using the function _set_se_translator.
It's OS specific so I'll not go into detail. Googling on the function name above will help anybody interested. It means we get a bit more information if a 'crash' occurs.
If the stack has context information left over from a previous exception on it when an exception is thrown the user could be exposed to two sets of context, which is confusing.
Our first stab at dealing with this potential hazard was for the method which returned the context information to return a copy of the stack and then clear the stack.
const CContextInformation CExceptionContextStack::GetAndResetContext();
But, before long another method had turned up:
const CContextInformation& Context() const
It was handy to peek at the stack in order to write it out to a debug window.
The stack could be cleared automatically in the context class constructor - if there is not an exception on the go the stack can be cleared.
CContext::CContext(..) : m_bExAtStart(std::uncaught_exception()) { if(m_bExAtStart) ClearStack(); }
It adds to the overhead of using context of course. It depends on the application whether or not this is a concern.
Originally we did not have a flag in the context class that recorded whether or not there was an active exception on construction.
CContextString::~CContextString() { if(!m_bExceptionAtStart && ExceptionBase::Uncaught()) { PushContext(); } }
This prevents the case where a context macro is encountered in code during the unwinding process (perhaps in a library call). This potentially results in a confusing extra message on the context stack.
Now that the technical bits are out of the way the system must be used, you still need to write appropriate, useful messages. What is the audience for them? The more wide ranging the worse it is.
You need enough information for the more able users to sort things out themselves, but error messages can scare less expert users who fear for their lives and freedom at talk of 'fatal' errors and 'illegal' operations.
We've found that it is worth provoking a few errors to see what messages emerge, by changing the code, deleting files or whatever it takes. We modify the results by changing text, adding more context macros and so on.
Redundant or inappropriate information is almost as bad as too little. An agreed policy helps.
One case we've seen is 'file not found', in most situations you need to know what the filename is and where it is trying to find it. Do you want to include the filename in the context message or the exception?
CONTEXT("Trying to open " + filename); if(!FileExists(filename)) { throw CException("Could not find the file " + filename); }
Here we get the filename twice which is cluttered. Throw in some more context and other redundant sloppiness and you have something that either needs careful reading or stays unread by the user and turns into a support call - this in turn makes the user feel less empowered and costs money.
So the hard bit is writing good consistent messages for the user. My advice:
-
Provoke errors (modify if they are not good enough)
-
Have a consistent policy.
We don't use contexts all the time. Our software is used by the marine community for various applications around navigation, planning, tide prediction and so on. The applications involve the importing and updating of data provided by third parties. Navigational charts are the obvious example.
It is these complex batch-like bits of the software, say importing 4000 charts, that most lend themselves to the exceptions and context error handling strategy. Something goes wrong deep down and the exception unwinds to a single point where it can be reported to the user or logged.
The overhead imposed by contexts does not worry us here.
In the day to day GUI use of our applications, when drawing a route on a chart we have far more control. Exceptional situations should not happen and performance is an important consideration. (Drawing performance is something that can separate our applications from that of competitors). We don't use contexts in this situation.
Notes:
More fields may be available via dynamicdata ..