Author : Steve Crocker
Page : 1 Next >>
by Steve Crocker
Introduction
Exceptions have been around in C++ for a while and are pretty common. If you use the STL or even just new you have been exposed to exceptions. Of course, like all things in C++, having a language feature does not necessarily clearly point one in the direction of the best use of that feature. I would not dare to call myself an expert or authority on exception handling, but as a user of C++ I've had some exposure to using exceptions.
This article will offer some insight into the use and potential misuse of exceptions. I highly recommend reading Scott Meyer's More Effective C++; there is a whole section devoted to exceptions. Bjarne Stroustrup's The C++ Programming Language Third Edition also has some excellent information on exceptions. I am assuming that the reader is familiar with the basics of exception handling and hopefully has read over Meyer's items on exceptions.
I also use a few terms from John Lakos's Large Scale C++ Software Design. This book is invaluable for understanding how a logical design should be translated into a collection of namespaces, header files, source files and libraries. I mostly refer to components and packages.
Overview of C++ Exception Handling
C++ Exception Handling is centered around the three keywords: try, catch, and throw, the general purpose of which is to attempt to execute code and handle unexpected exceptional conditions, hence the name. This consists of utilizing a try block (with its attendant handlers). Here's a simple code snippet that should look familiar:
try
{
if ( ! resourceAvail )
throw MyExceptionClass( "Resource Is Not Available" );
}
catch(MyExceptionClass& myException)
{
// resource was not available, do something cleanup
throw;
}
The code checks to see if a resource is available and if not, throws an exception. The handler for the MyExceptionClass presumably does something meaningful about the exceptional state. From this over-simplified example we can see a typical use of exceptions which is to prevent continued operation if the program cannot obtain the required resource. Another example of this use of exceptions is new(), which will throw the standard exception bad_alloc if the required amount of memory is not available. (Unless, of course, you've changed the new handler.)
Implications of Using Exceptions
To support exceptions and stack-unwinding, the process of calling destructors on leaving a scope, compilers put in some initialization code to ensure that if a routine may return via an exception it will properly call destructors. This seems to imply that stack-unwinding is a big part of the fixed cost regardless of whether or not you use a try/catch block. Even if you only use primitive types, if you make calls to functions, which is highly likely, then you probably end up paying this cost. So it is probably best to assume that this fixed cost is a part of the cost of a function call, and potentially, scope change if that scope change has local variables.
This means there is additional code being executed. This can consist of function calls that the compiler automatically inserts into the code. Some compilers may allow this code to be generated inline to improve performance, such as C++ Builder. So if we are probably going to be paying this cost, then why worry about how many try/catch blocks there are? Because it is best to partition error handling code from regular execution code. This keeps the intent of the code clear.
Another important fact about exceptions is that they affect program flow control. This is very significant particularly during the debugging process. A thrown exception can transfer control to a catch in a very distant location as well as, of course all the way up to main(). This can wreak havoc with figuring out the origin of the exception. Just consider this innocuous source listing:
try
{
SomeFunc();
AnotherFunc( 16 );
YetAnotherFunc( EvenMoreFunctions( 2 ) );
}
catch(...)
{
// catch all exceptions and cleanup
throw;
}
Now try debugging this when an exception is generated by a function that EvenMoreFunctions() calls. And it only happens some of the time. Granted, if you're in a debugger you can just enable the 'Break on C++ Exception' option and viola. However, if this a bug report from a tester, or worse, customer, and it rarely happens, well then you have some trouble.
It is not just the throw of an exception which impacts program flow control. The catch clause or more specifically, clauses can have a significant effect as well. When the exception is thrown it will be transferred to the first matching catch block of the exception type thrown. This can be conceptualized as a special kind of switch statement; and a switch statement affects flow control.
Exception Specifications
When I first started learning about exception specifications I flip-flopped quite a bit about how useful they are. I could see the benefits from a client perspective, but the implications imposed on the component writer made me reconsider where exactly they should be applied.
The essential benefit of an exception specification is that clients of a component know exactly what exceptions may be thrown from a function or a method. This can be great from the client perspective of handling exceptions, because the client knows exactly what exceptions, if any, may be thrown. However, as a component implementer, the cost of guaranteeing meeting that expectation may be high.
For example, let's consider the following code.
class MyClass
{
void MyMethodWithOnlyOneExceptionSpecification(void)
throw (MyExceptionClass);
void MyMethodThatThrowsNoExceptions(void) throw();
};
As a client of MyClass you can say, "Great! I only have to worry about MyExceptionClass exceptions coming out of MyMethodWithOnlyOneExceptionSpecification() and no exceptions will be thrown from MyMethodWithThrowsNoExceptions()."
In actuality, the first exception specification says that exceptions of type MyExceptionClass or derived from MyExceptionClass may be thrown from that method. But maybe that isn't such a big deal after all. As a client you may only care about the generic MyExceptionClass.
However, to implement this specification we will most likely have to use a try/catch block within the implementation of these methods to enforce the exception specification. Otherwise we run the risk of std::unexpected() being called, the default behavior of which is to terminate the application. Clients of our class may override this default behavior, but as a component implementer we cannot make this assumption. And since aborting the program execution is usually much more catastrophic than letting an exception propagate all the way up we have to fall back on using the try block. This means you could be imposing performance penalty for your exception specification, above and beyond the performance penalty of the exception specification, that is. Granted, maybe you could conditionally compile the try block in debug versions of your component and or package, but that seems rather dangerous.
Because of this problem, it seems best to avoid exception specifications for most components. So where does it make sense to use exception specifications? My experience has indicated that if you need to enforce an exception specification, either of a specific exception class or no exceptions, then you should probably do so at the highest level components of your package. And do so only when you know you must. Another potential location for using exception specifications is in thread routines as throwing an exception out of a thread routine is analogous to throwing one out of main().
Real-time systems, such as games, may benefit from not permitting exceptions to be thrown from certain portions of their API. This is because of the effect on flow control exceptions have. In this case it may be beneficial to use exception specifications to enforce this.
Designing With Exceptions
When implementing the packages which make up your application, such as your core graphics engine, it is very useful to define an exception hierarchy to utilize. This is useful for delineating kinds of exceptions as well as translating error codes into useful string messages. An example of this is the standard exceptions of the Standard C++ Library.
A simple base class exception type can contain an error message string into which you can place your exception information. Specifying constructors which take parameters that specify the file and line number ( which typically can obtained from __FILE__ and __LINE__ ) and a string message allows for an easy way of storing the origin of the exception. You can also put a simple method for logging the exception which can be called in the constructor of a concrete exception class.
I've also defined exception classes which translate DirectX error codes into a human readable message. If a component in the game engine package throws an exception it is always of a type derived from the base exception class of the package.
In general, if you are going
Page : 1 Next >>