The classics::exception class provides a way to signal errors and preserve useful logging
information concerning the nature of the error.
This class embodies an error handling system that’s built around these two design principles:
Here is how the exception class is meant to be used.
When an error occurs, create an exception object. The constructor takes general demographic information. Then add formatted text into the error object detailing the situation. Finally, throw the error object.
void* data_t::access (int index)
{
if (index >= Count || index < 0) {
classics::exception X ("Classics", "Subscript Error", __FILE__, __LINE__);
wFmt(X) << L"The subscript " << index << L" is not >=0, <" << Count << endl;
throw X;
}
return (index*Elsize) + static_cast<byte*>(Data);
}
First construct the error object:
classics::exception X ("Classics", "Subscript Error", __FILE__, __LINE__);
The first argument is the “module”. This identifies which software component or subsystem is reporting this
error. I note here which library (Classics, RatWin, Tomahawk, Conquer) is involved.
The second argument is the title of the error. This should be a short name.
The last two arguments specify the location of the error. Here, the standard pseudo-macros are used to note the actual file name and line number.
The second line is a bit more complicated.
wFmt(X)
<< L"The subscript "
<< index
<< L" is not >=0, <"
<< Count
<< endl;
The first part, wFmt(X), starts things off. This creates a temporary formatting object, as explained under
string formatting.
Various values and modifiers can be sent to the formatting object using the familiar << syntax. In particular,
the example uses endl to terminate the information. However, line breaks in the formatted text is
just part of the formatted text, and is not necessary to the internal representation of the exception object.
Note that text literals are prefixed with the L symbol. This is because wFmt is a wide
formatting stream, and can take Unicode strings. Although designed to accept plain string literals as well,
the compiler had some problems with it. As shipped (public build 5 compiled under VC5), plain character literals will not give any warnings but will
be treated no differently than a void* or object pointer— it will print the hex value of the address, not the contents
of the string.
A derived class exists specifically for Win32 errors. The classics::win_exception can be constructed
with an error code number, and it will automatically add the text corresponding to that error. Easier yet, you can
leave off the value and the constructor will call Win32’s GetLastError itself.
Eventually, you need to catch the error. When you do, the member value() will return a ustring
containing all the error information. This information is in a delimited report,
so you can parse out the various fields to produce a nice display. The raw string is sort of readable, as it contains
all the information you put into the object plus control-code delimiters.
A step beyond value(), which just gets the string, is a registered function to display the
error for the user. The show() function will do this. So, upon catching an exception, you just
have to call show.
try {
...
}
catch (classics::exception& X) {
X.show();
}
Unlike other error systems where low level errors are replaced by higher level errors, all the information is preserved. The exception in this example will contain three stanzas, each a complete error record. The user can see what operation failed, what low-level detail was the actual cause of the problem, and everything in between, allowing him to understand the connection between the high-level operation and the nuts and bolts of the actual failure.
Each stanza is a complete error record, containing both structured values and
unstructured text. The structured values are like a program’s environment block,
containing a list of named strings. Every stanza will contain the module,
name, file, and line information (provided by the constructor or operator())
as structured values. The unstructured text is just plain text, and can contain
any information you desire. Generally this will be details about the operation
that failed.
In the example in the Concept section, the resulting stanza will look something like this:
module=Classics
name=Subscript Error
file=example.cpp
line=147
The subscript 27 is not >=0, < 20.
+= operator or
the wFmt feature as seen in the example. Additional named values can
be added using add_key, as in
X.add_key (L"Thread ID", ThreadID); X.add_key (L"foozle", as_wstring(foozle));
The value should be a simple value of some kind, not an elaborate
formatted string. The add_key function has two overloaded
forms: one takes a ustring, the other an int.
For anything else, use as_wstring1 to produce a string value.
Showing the Exception
A fundimental thing you'll want to do with an exception object is present it to the user for
viewing. To do this, simply call the show member of the exception object.
catch (classics::exception& X)
{
X.show();
}
So just what does show do? That really depends on the nature of the program.
A command-line utility needs to write to the standard error stream, and a GUI-based program should
display the information in a manner consistant with the user interface of the program.
In order to decouple the exceptions from the program containing them, this mechanism needs to be
definable elsewhere. A simple way is used here: The show_function static member
points to the function which displays the error. By default, this will send output to the standard error
stream. The main program can repoint it to use any desired mechanism.
value, but this is a
pain. Naturally, code is already supplied to do this, both in the exception
itself and packaged as class exception::iterator.
You will want to extract information either because you are writing a “show” function, or because code wants to inspect (and react to) values found in the exception. For example,
catch (classics::exception& X)
{
string errval= X.get_value ("GetLastError");
// ...
}
If the thrower included a named value as such:
X.add_value ("GetLastError", ::GetLastError()); //Win32 error code
(which the win_exception derived class does automatically), then the catcher
can obtain this value as shown here. Note that get_value always returns a
string, and it's up to the caller to deal with it as such, or turn it into a
number. Also, the caller will need to cope with the possibility of an empty
return string, or with multiple values concatenated together (see the
reference for details).
For something that’s more elaborate, as well as more efficient when more
than one value is needed, the iterator can chop up the exception
into individual stanzas and then return information on each stanza without
having to re-parse each time.
Auto-logging Additional Values
Well, it doesn’t. Consider values that are useful to your program, but are unknown to the low-level code doing the throwing. This would be cumbersome to add intermediate catches simply to report these values, but it doesn’t always work either! Consider what happens if something is handled below the level at which the information is known.
For an example, consider putting the “document name” in each exception, since the program will have multiple documents open at one time. However, lower level code doesn’t know or care how it was invoked, or which document it’s being called to work on. So how does this information get into the exception object?
void deeper()
{
exception X ("demo", "This is a planned failure", __FILE__, __LINE__);
throw X;
}
void deep()
{
try {
deeper();
}
catch (exception& X) {
X.show();
// exception handled here.
}
}
void toplevel()
{
string docname="foobar.txt";
//...
try {
deep();
}
catch (exception& X) {
X.add_value ("document name", docname);
throw;
}
}
The catch in toplevel knows the information, but the exception is handled
before the stack unwinds to this point. So, when show() is called, the user
does not get to see this information.
To address this issue, the exception class includes features that allow
unknown things to be logged at the point the exception is created.
The simpler of the two mechanisms is the setup hook.
The setup hook allows the application to change the construction work of all exceptions, even
those created by lower-level code, code in DLL’s that have already been compiled, etc. Details are
described in the documentation for setup_hook.
When an exception is created, all existing callbacks are called. The callbacks add this information to the exception object. A helper class exception::register_callback will register the callback when constructed and revoke it when destructed.
void toplevel()
{
string docname="foobar.txt";
exception_value_logger<string> logger ("document name", docname);
exception::register_callback xx1 (logger);
//...
deep();
}
Now any time an exception object is constructed, logger will be triggered, and
it adds the desired information. Just declare an
exception::register_callback
object in the same scope as the variable to be watched, and the rest is
automatic.
as_string or as_wstring will do, but as_wstring is more efficient
because the internal implementation uses wide strings.