Journal Articles
Browse in : |
All
> Journals
> CVu
> 154
(12)
All > Topics > Programming (877) 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
Author: Administrator
Date: 03 August 2003 13:15:59 +01:00 or Sun, 03 August 2003 13:15:59 +01:00
Summary:
Body:
This article is based on a concept presented by Andrei Alexandrescu at the 2002 ACCU conference [Alexandrescu]. The basic idea Andrei presented was to store contextual information about the execution stack that can then be accessed in the event of an exception being thrown. In this respect, the idea is very much like using a debugger to display a call stack, except that the information is developer specified context.
Imagine some highly simplistic code that represents the process of building a house; the code might be something like:
int main() { int some_house_id = ... // Get a house identifier BuildHouse(some_house_id); } void BuildHouse(int house_id) { int num_walls = ... // Establish how many walls // are needed. for(int i = 0; i <= num_walls; ++i) BuildWall(i); } void BuildWall(int wall_id) { int bricks_required = ... // Establish number of // bricks required for(int i = 0; i <= bricks_required; ++i) LayBrick(i); } void LayBrick(int brick_id) { BrickPos bp = GetBrickPosition(brick_id); // lay cement, place brick, point join etc. } BrickPos GetBrickPosition(int brick_id) { BrickPos bp; // calculate the brick position. if(bp->Invalid()) throw InvalidBrickPosException(); }
Now if an invalid brick_id value gets into GetBrickPosition(), it might be reasonable to throw an exception. It is likely that LayBrick() doesn't know why the brick_id value is invalid, so the exception should be caught further down the call stack. In fact, because the inability to lay a brick means it is difficult to complete the house satisfactorily, it might be desirable to catch the exception in main() and then to indicate to the user that the house building process has failed, and in what circumstances it has failed. So the objective of providing contextual information with exceptions is to be able to generate output in a friendly and informative format such as the following:
Error: No brick position identified for brick #brick_id While getting brick position for brick #brick_id While laying brick #brick_id While building wall #wall_id While building house #house_id
Ideally, in order to provide the kind of contextual information described above, the code would be rewritten something like the following:
int main() { // Some code to get a house identifier try { DO_IN_CONTEXT("While building house") BuildHouse(some_house_id); } catch(const ContextException & e) { std::cout << e.what() << std::endl; } } void BuildHouse(const int house_id) { // Code to establish how many walls are needed. for(int i = 1; i <= num_walls; ++i) { DO_IN_CONTEXT("While building wall") BuildWall(i); } } void BuildWall(const int wall_id) { // Code to establish number of bricks required for(int i = 1; i <= bricks_required; ++i) { DO_IN_CONTEXT("While laying brick") LayBrick(i); } } // etc.
At first appearances, it might be thought that DO_IN_CONTEXT is a macro concealing a try-catch block, which catches the propagating exception, adds its contextual information to the exception message, and then throws a new exception. One of the main arguments Andrei put forward was that this would be an unnecessary complication; instead it is preferable that the macro creates an automatic object whose destructor does the necessary work during stack unwinding. The idea was to make use of std::uncaught_exception() in the destructor of this automatic context object to determine if an exception is propagating, and therefore whether to add the context information it contains to the exception.
After the conference, Alisdair Meredith started a thread on comp.lang.c++.moderated discussing this concept [google1]. The upshot of this discourse was that the original idea was flawed. Conceptually, the context object might have looked something like the following:
class Context { public: Context(const std::string & context) : ex_at_start_(std::uncaught_exception()), context_(context) {} ~Context() { if(std::uncaught_exception() && !ex_at_start_) { // An exception occurred after the constructor // executed and before the destructor. // Add the context information held to the // exception } } private: bool ex_at_start_; std::string context_; };
The problem as discussed on c.l.c.m is that there is no way to access the propagating exception without catching it; precisely the situation this object was designed to avoid. The solution Andrei proposed is to create a context stack instead of adding the contextual information directly to the exception. This could either be some kind of static object in a single-thread application, or thread local storage in a multi-threaded application.
This mechanism can still make use of an automatic context object. Instead of storing the context information locally in the object, the context object's constructor pushes it onto the context stack. Then the context object pops the last context item from the stack if its destructor is reached without an exception being thrown. If an exception is detected in the destructor, the context stack is left unchanged and the whole of the context information can be retrieved from the stack at the point where the exception is handled. A basic implementation of this idea for single-threaded usage is given below.
// rhex_static.hpp #ifndef RHEX_STATIC_HPP #define RHEX_STATIC_HPP #include <string> #include <deque> #include <ostream> #define DO_IN_CONTEXT(str) \ rhex::Context context(str); #define DO_IN_SCOPED_CONTEXT(str) \ { rhex::Context context(str); #define END_SCOPED_CONTEXT } // rhex::ExContextHolder namespace rhex { class ExContextHolder { public: // Queries static ExContextHolder& Holder(); std::string LastContext() const; void DumpContexts(std::ostream& dest); // Modifiers void AddContext(const std::string& context); void PopLastContext(); private: // Constructors and destructors ExContextHolder() {}; ExContextHolder(const ExContextHolder& rhs); ExContextHolder& operator= (const ExContextHolder& rhs); // Data members std::deque<std::string> contexts_; }; } inline rhex::ExContextHolder& rhex::ExContextHolder::Holder() { static ExContextHolder context_holder; return (context_holder); } inline std::string rhex::ExContextHolder::LastContext() const { return (contexts_.empty() ? std::string() : contexts_.back()); } inline void rhex::ExContextHolder::DumpContexts (std::ostream& dest) { while(!contexts_.empty()) { dest << contexts_.back() << std::endl; contexts_.pop_back(); } } inline void rhex::ExContextHolder::AddContext (const std::string& context) { contexts_.push_back(context); } inline void rhex::ExContextHolder::PopLastContext() { if(!contexts_.empty()) { contexts_.pop_back(); } } // rhex::Context namespace rhex { class Context { public: // Constructors and destructors explicit Context(const std::string& context); ~Context(); private: // Data bool exception_present_; }; } inline rhex::Context::Context(const std::string& context) : exception_present_(std::uncaught_exception()) { if(!exception_present_) { rhex::ExContextHolder::Holder().AddContext (context); } } inline rhex::Context::~Context() { if(!std::uncaught_exception() && !exception_present_) { rhex::ExContextHolder::Holder(). PopLastContext(); } } #endif // RHEX_STATIC_HPP
A std::deque object is preferred as the stack container because it offers the opportunity to add more extensive access features to the ExContextHolder instance should it be required. A std::stack container adapter could be used, but as the default container for this adapter is a std::deque, there is no real benefit to doing so in this case.
In most cases the DO_IN_CONTEXT() macro is sufficient. In some cases it may be necessary to use the pair of macros DO_IN_SCOPED_CONTEXT() and END_SCOPED_CONTEXT (for users of Borland C++ Builder, a slightly modified version is required due to an apparent library bug[1]. These allow the automatic Context object to be wrapped in a scope, allowing for nested contexts within a single function, although in general needing to do this probably indicates a design flaw as it implies the function is doing too much work. It allows for constructs such as:
void foo() { DO_IN_SCOPED_CONTEXT( "While in outer scope" ) // Some processing which might produce an // exception DO_IN_SCOPED_CONTEXT( "While in inner scope" ) // Some more processing which might produce an // exception END_SCOPED_CONTEXT END_SCOPED_CONTEXT }
which might produce output such as:
An error occurred While in inner scope While in outer scope
As an interesting aside, the use of the Singleton pattern in this work was an issue of some considerable concern to the author. This particular use appears to be an extremely rare case where use of a singleton can be justified although is not completely necessary. The code presented here is a work-around required because of an ineffective language feature [Sutter2002], std::uncaught_exception(), and so is designed to be much like a language feature; easy to use and non-intrusive.
So why use a Singleton? It might, for example, be preferable to force the client to create an instance of the context stack at the beginning of each critical execution path, which is then passed down the call chain as an additional parameter. But this imposes a clumsiness and additional overhead for the client programmer. Now users have to decide when to create context stacks, where to pass them as parameters and where they should be part of the state of important classes. Furthermore, context stacks may frequently need to be passed to functions that don't actually need them, so that they can be passed on down the call chain. Experience shows that if this kind of feature is hard to use, developers just won't bother. For single threaded applications, a Singleton is a simple, non-intrusive and easy to use solution.
There is a question mark over how useful providing this sort of context information is. It can be argued that this provides very little useful information to the end-user, and nothing that a developer can't get from other sources. In this case, it would be difficult to argue that the extra overhead involved was worthwhile. In complex applications, however, where exceptions may be handled a long way from their source, the extra information is invaluable. Even with the simple house building example presented earlier, it is easy to see the benefit. Imagine if this formed part of a larger system, and houses were not the only type of building that could be constructed with walls. The bricklaying functions know nothing about what structure the bricks are for, so the exception either has to have no information about the type of structure, or coupling needs to be increased by passing information up the execution stack. If this formed, for example, part of an automatic batch job or similar, which would be a preferable message for the operator upon their return: one saying 'Invalid brick position' or one saying 'Invalid brick position while building house X'? Picture also the bug report that your customer files; which message attached to the report will be easier for the maintainer to get to grips with?
For many applications, where the additional overhead imposed is acceptable, the benefits will be considerable, either through better information, or through a better design because of reduced coupling between functions, or both. The author's own experience of using this mechanism on small applications has already shown benefits. The very first unintentionally generated exception encountered when using this code immediately showed that some unexpected recursion was happening, and saved considerable time in hunting down the source of the problem.
The implementation presented here is very simple, and there is clearly scope for a much more advanced system for large applications. One idea that Andrei has suggested [google2] would be to store a generic base class in the context stack rather than strings. The base class would specify some kind of simple output interface, similar, for example, to the standard printString method in all SmallTalk classes. Developers would then create more complex, application specific context classes derived from this base class, which know how to display their context information.
Where to use such context providers is also an interesting question. The ideal situation would be to only deploy context objects along execution paths that could result in an exception, and where the additional context provided will be useful. In reality, the complex nature of most software makes it difficult, if impossible, to identify all possible exceptional execution paths [Sutter2000]. Instead the emphasis must be on using context objects at key points; where either significant runtime information can be added to the context stack, or along the principal execution paths for the software.
Thanks to Pete Goodliffe for encouraging me to submit this article and for providing a helpful informal review of it.
[google1] http://groups.google.com: thread start id: 3CB69B73.9E8BA09C@uk.renaultf1.com
[1] Rather annoyingly, std::uncaught_exception() doesn't seem to work properly with Borland C++ Builder. I found that it always returned false even when an exception was propagating. The upshot was that the contexts that should have been left on the context stack were popped when the various automatic Context objects were destroyed during stack unwinding. The slightly ugly solution to this is to push and pop the context strings manually, avoiding the use of Context at all. This requires a pair of macros for both scoped contexts, but obviates the need for unscoped contexts, e.g.,
#define DO_IN_CONTEXT( str )\ rhex::ExContextHolder::Holder().AddContext( str ); #define END_CONTEXT\ rhex::ExContextHolder::Holder().PopLastContext();
Notes:
More fields may be available via dynamicdata ..