Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Custom Exception Types


February, 2005: Custom Exception Types

Kurt Guntheroth holds an M.S. in computer science from the University of Washington and has been developing commercial software for 20 years. He can be contacted at [email protected].


It is a recurring design pattern that infrequently occurring events can prevent performance of some operation, and the code that responds to this failure is in high-level logic, far up the function call stack from the code that detects the event. The exception-handling mechanism in C++elegantly implements this pattern; it signals the event, delimits the affected operation, concentrates runtime cost in the infrequently executed exceptional case, and frees resources while unwinding the call stack on the way to the handling code. It is a mistake (with high runtime cost) to use C++ exception handling for events that occur frequently, or for events that are handled near the point of detection.

An exception type defines a package of information provided by the throw statement that raises the exception, for use by the try/catch block that handles the exception. A well-designed exception type should identify the exceptional event in enough detail—saying what went wrong and where the exception occurred—for users or developers to correct the problem. A good exception type facilitates efficient throwing and handling. It is robust enough to work properly, even under the difficult or unexpected conditions likely to give rise to exceptions.

The try/catch block defines the scope of an operation that can be interrupted by a thrown exception, and provides the code that handles exceptions. There are only a few conceptual ways to handle an exception. If all future operations depend on the one that failed (initializing a window, for instance), the program must stop. If operations are independent (such as commands), the program can fix something and retry the failed operation, abandon the failed operation and perform the next one, or perform an alternate operation.

Why Not std::exception?

The C++ Standard Library defines a rich family of exception classes. A few of these are thrown by C++ on memory allocation failures or improper casts. In a Standard Library extension or a simple program, it is appropriate to throw an exception type derived from std::exception. In large programs, there are good reasons to use a project-specific exception type instead.

The standard exception classes form a hierarchical catalog of the sorts of design and runtime errors that can give rise to exceptions. The std::exception class hierarchy is user extensible; derived classes represent new exceptions or exception categories.

At development time, the derivations of std::logic_error inform you of design errors. They are most useful in library code, where library developers may be disconnected from other developers. Most logic errors are discovered and removed during development. Exceptions in production code arise mostly from unexpected problems in the program's runtime environment: resource shortages, security violations, hardware failures, and so forth. Runtime exceptions derive from std::runtime_error. Since the catch clause that handles an exception depends on the exception type, this design works best if logic errors are handled one way and runtime errors another.

What matters is whether the failing operation can be retried or discarded, or the program must be stopped. The appropriate action depends partly on the meaning of the interrupted operation in the overall program, partly on the details of the specific exception, and hardly at all on whether the exception is a kind of logic error or a kind of runtime error. There is a mismatch between the standard exception hierarchy, which offers to select recovery code based on the taxonomy of exception causes, and the needs of the program to select from a few recovery strategies, with little regard to the cause of the exception.

std::exception describes the "what" of an exception in two ways—through the std::exception-derived class type, and through an optional string argument to most exception class constructors. Neither of these two forms of "what" information lends itself to efficient classification in the handler. Comparing strings is slow. It further requires the developer to always remember to pass the additional information to the exception constructor. Using derived class types to select a handler means multiple catch clauses with duplicated code.

If the code has many throw sites and only a few catch sites (a typical pattern), knowing where the exception was thrown is helpful for debugging. The "where" of an exception can be a program subsystem, function name, filename and line, or perhaps an entire callstack trace plus thread information. std::exception doesn't provide this directly. It is possible to put "where" information in the exception constructor's optional string argument, as in Listing 1.

Using std::exception's optional constructor argument for "where" information means relying on programmers to provide the information at every throw point, and makes it harder to put other ("what") information in the same string.

To support the responsibility for formatting a message, std::exception has a virtual what() method. std::exception uses dynamically allocated memory and supports extensibility through derivation, so it has a virtual destructor. Thus, every standard exception class instance contains a vtable pointer along with its other data. An instance of std::exception doesn't fit into a CPU register like an integer does. When copying or deleting an std::exception instance, the compiler is more likely to generate a function call instead of speedy inline code. This problem is even more acute on modest embedded processors, where efficiency is most important.

std::exception's virtual destructor is especially troublesome. In any scope that constructs objects (potentially any matched pair of curly brackets, but typically a function body), the compiler must generate additional code to destroy the objects if an exception is thrown within the scope. No extra code is needed in scopes that don't construct any objects, or whose constructed objects don't have destructors. The throw statement constructs an object (the exception class instance). Some compilers thus generate extra code for any function that throws an exception, if the exception type has a destructor (like std::exception). For compilers with this behavior, the solution is to call a function that throws the exception. The compiler may generate extra preamble and postamble code for the Throw() function in Listing 1. The Throw() function insulates the many calling functions unless they create other local objects with destructors.

Exception classes derived from std::exception accept an optional constructor argument of type std::basic_string<char>&. Constructing such a string generally calls the memory allocator; a troubling choice for many reasons. Calling into the allocator is expensive, typically consuming thousands of CPU cycles. More important, the exception class becomes coupled to the memory allocation subsystem. You can't pass optional information to an std::bad_alloc exception because the allocator has already failed. Further, if the heap is corrupted, preparing to throw the exception may cause the program to crash, just as it is about to give useful information.

The standard exception classes make an irrevocable choice of character width by accepting an std::basic_string<char> constructor argument, and by defining the what() method to return an std::basic_string<char>. This adds complexity to applications using wide characters. It isn't obvious that an exception class must embed the responsibility for formatting error messages, in addition to conducting "what" and "where" information to the try/catch block.

Simple Exception Type Alternatives

It isn't necessary to throw a class instance as an exception. The exception-handling mechanism can throw and catch any type. Throwing exceptions of integral type is both powerful and efficient. The thrown values can belong to an enumeration, can be Windows string resource IDs, or can be error codes from UNIX _errno or Windows GetLastError(). Throwing an integer requires no function call for constructing or copying the data.

Integral exception types are easy to catch. Instead of multiple catch clauses for the many derivations of std::exception, a single catch clause handles all integral exceptions. Inside the catch clause, a switch statement can efficiently classify the exception value to provide specialized processing. Switch statements can handle multiple cases with one statement block, provide default handlers, and don't have complex type-matching rules.

The principal weakness of integral exception types is that they provide little in the way of "where" information. This problem is not insurmountable. The "where" of the exception can be provided by optional file and line arguments to a Throw() function. The Throw() function can log this information as in Listing 2. The actual values of the file and line arguments come from the C++ preprocessor macros __FILE__ and __LINE__, which always refer to the current file and line number. A complete stack trace can also be recovered by inspecting the executable image's symbols and by walking the stack, at least on Windows and Linux. (A description of this technique is beyond the scope of this article.)

A Simple Exception Class

A well-designed exception type describes what happened and where. It is efficient to throw and handle. Classifying exceptions in the handler should be simple, efficient, and avoid duplicating code. A good exception type avoids interactions with major subsystems, such as the memory allocator, that may themselves be the cause of the exception. The exception type's extension mechanism ideally avoids the runtime cost of polymorphism. There should be an extensible way to format exception text for display that doesn't mandate a specific character width or encoding. Listing 3 is a first candidate for such an exception type.

Although this exception type is a class instance, its only member is an int because the file and line are logged and discarded in the constructor. An instance can be constructed and copied efficiently. It has no virtual functions and no destructor. The required "what" constructor argument is a numeric exception code to distinguish one exception from another. This type can accommodate a virtually unlimited number of distinct exceptions as unique integer codes. As with integral exception types, the exception code can control a switch statement for efficient classification of exceptions in the try/catch block.

An Efficient Exception Class

The simple exception class has some weaknesses. It assumes the availability of a logging subsystem at the point where the exception is thrown. This assumption can be violated, as when there is a problem with the filesystem or memory allocator. It is somewhat safer to defer logging to the handler, when the stack has been unwound and hopefully resources reclaimed. To perform logging in the handler, the exception type must retain the "where" information. This can be done efficiently by keeping a pointer to the filename argument.

The fast_exception class of Listing 4 is similar to simple_exception. It is still simple and small enough to be reasonably efficient. However, this efficiency is bought with some risk. The constructor's file argument is expected to come from the C++ __FILE__ macro, which creates a literal character string in the initialized data storage area. The exception class stores only a pointer to this data. You can think of a literal string as being constant. But C++ doesn't enforce read-only access to literal strings; a buffer overwrite can compromise this data. Further, C++ cannot prevent a user from passing any value as the file argument, including a pointer to local data that will become garbage when the stack unwinds during exception processing.

Instead of a virtual what() method that returns a string, a project-specific exception header can define iostream inserters or other methods taking the exception as an argument for formatting a displayable string representation of the exception. This decouples formatting exception data from throwing and handling the exception. On Windows, the "what" codes can be string resource IDs. This allows—but does not require—you to associate a formatting string with the exception. The exception-formatting method can parse the file and line into the string using the Windows FormatMessage() function, sprintf(), or equivalent.

The first template in Listing 5 is a stream inserter. The second method formats the exception into a provided fixed-length buffer and returns the beginning of the buffer.

A Conservative Exception Class

The design of a general exception-handling class is influenced by your degree of paranoia. An important question is, "How much of the environment shall we rely upon when an exception is thrown?" After all, if the environment wasn't broken, the program wouldn't be throwing exceptions. Again, any exception class that relies upon the memory allocator accepts a risk, especially when reporting a bad_alloc exception.

You can extend this paranoia to resources such as the stack, GUI, filesystem, networking subsystem, and so on. All such resources might be corrupt or unavailable. A really fastidious embedded-system environment might not rely upon any of these things. Its exception constructor might log exceptions to reserved, nonvolatile memory that can subsequently be accessed with no support beyond execution of CPU cycles.

In a mature application running on a more robust operating system and platform, less paranoia is called for, but it still forms a consideration. I follow these guidelines:

  • Don't call a lot of functions in the exception constructor, lest the stack run out.
  • Minimize use of heap and other system resources at least until the handler, when the stack has been unwound and presumably some resources freed.
  • Log to a text file rather than to a complex network-based logging program.

A paranoid view of exception-constructor arguments is appropriate, too. The guaranteed lifetime of pointer arguments is bounded by the constructor call. Really fussy exception classes (such as Listing 6) copy their arguments even if they are expected to be (for instance) literal strings. Copying trades size and speed for safety. In spite of its correctness, I cannot recommend this degree of caution without reference to the specific implementation. Speed considerations, the operating environment, and maturity of the project must be considered when trading efficiency against paranoia and perfect usage.

For big projects, or where speed, simplicity, or pure paranoia is an important consideration, a custom-designed exception type is an appropriate choice. Source code that implements a more mature class is available at http://www.cuj.com/code/.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.