Jump to content

  • Log In with Google      Sign In   
  • Create Account

Like
2Likes
Dislike

Understanding C++ Exception Handling

By Steve Crocker | Published Mar 05 2000 01:33 PM in General Programming

exception exceptions code thread handling block c++ try using
If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource

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();
   };
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 to throw an exception, you might as well gather up as much knowledge about the exception then because you are already paying a huge performance penalty for throwing the exception. Also, if the act of throwing your exception happens to cause memory exhaustion, there is guaranteed to be enough memory free to throw a bad_alloc exception. My experience has been that you'll likely get terminated by the operating system before bad_alloc is thrown, however in either case you're already in a world of hurt.

I'll admit that I've opted-out of using exception specifications in my DirectX wrapper package in order to avoid adding superfluous try blocks.

Also, since the exception classes themselves log the error message I can make my catch blocks either catch-all handlers or for my base exception class and still log exception information automatically. It is probably best to use the specific handler in places where only your own exception classes will be thrown and use the catch-all when making calls into APIs which may be throwing anything; this second case includes calling new() and its potential throw of bad_alloc. And I generally have one catch handler for the try block in order to simplify program flow control.

"Resource allocation is initialization" or Stack-Based Resource Management

This technique, as described Margaret A. Ellis and Bjarne Stroustrup in The Annotated C++ Reference Manual, by Stroustrup in The C++ Programming Language Third Edition and further elaborated on by Meyers in More Effective C++ is well documented. I usually prefer to think of it as Stack-Based Resource Management because it tells me a little more about what is going on and what the goal is.

A great example of this is the Standard C++ Library's auto_ptr() template class, which is specifically designed for this purpose. Here's an example of how it is typically used:

void Func(void)
   {
   auto_ptr myClass( new MyClass( ... parameters ... ) );
  
   myClass->Something();
   }
There is a number of cool things about this simple example. First, we don't have to worry about deleting the heap object in the case of an exception or normal execution; the auto_ptr's destructor handles this for us. This also means we have one path of execution regardless of whether or not any exceptions occur; either memory exhaustion or those thrown by the constructor of the class. This solution also scales well to having many heap allocated local objects. Even better, we do not have a try block! So we've already simplified the code significantly.

By using a different kind of smart pointer ( auto_ptr does not necessarily have the best copy semantics for this ) we can also apply this technique to classes which have multiple heap objects as members. This way we can properly handle construction without memory leaks and without a try block. Granted, maybe allocating multiple heap objects is going to cost more in terms of time than the try block initialization is, so maybe its a bit unnecessary from a performance standpoint, but it does make the constructor less cluttered.

Another common use of this technique is for synchronization mechanisms such as critical sections. If a portion of code or an entire routine needs to be synchronized, a simple object can be used to enter the critical section in the constructor and leave the critical section on destruction. I've seen this referred to as a guard or lock.

The fundamental is what Meyer's succinctly describes in More Effective C++ Item 9 - use destructors to prevent resource leaks. I usually tend to think of it as putting code which must execute, regardless of how a block is exited, into the destructor of an object on the stack. In auto_ptr the resource is memory, in a critical section the resource is the lock on the critical section. No doubt you can think of other examples which may be more involved than these simple examples.

This is really one of the most useful aspects of exception handling: stack unwinding. And not only does it make the code easier to read and understand it also can be used to eliminate try blocks, which can help improving application performance.

Interactions with Threads

There are a few important things to keep in mind when dealing with exceptions in a multi-threaded program. Most are pretty straightforward but if forgotten can cause a number of problems.

First is that each thread routine is akin to main() and you should not allow exceptions to be thrown out of the thread routine. Provided you created the thread using _beginthread(), and properly initialized the runtime library, your application will exit indicating abnormal program termination. Otherwise you will likely cause the operating system to display an unsightly message about your application and offer to terminate it for you. This is actually worse than throwing an exception from main() which should just indicates abnormal program termination.

Trying to use an exception specification on the thread routine in order to get it to call your installed unexpected handler does not seem to work. The application just terminates executes as if no exception specification existed. I'm not sure if this is considered a bug in my compiler or not. The bottom line is that each thread routine should be created using _beginthread() and include a try-block and a catch-all handler just like main() generally does.

Next is that each thread routine has its own stack, and hence exception chain. This means that the current exception has to be per thread data and re-throwing an exception is, obviously, only valid within a single thread. The implication is if you have multiple threads which may be working with the same component or group of components and one causes an object to enter an invalid state due to an exceptional condition, your other threads will probably not be aware of this, even if you are properly synchronizing access.

The problem is that an exceptional condition in one thread has no way of indicating to another thread that such a condition has occurred. And when your other thread is scheduled it will probably end up encountering an exception as well. This means that an exception in one thread should probably stop the other related threads as well. Of course, subtle problems can arise in how those other threads are stopped. I've usually used an approach where one thread, maybe the main application thread, is ultimately responsible for stopping the dependent threads if one of them stops running. It may also be responsible for stopping other threads and itself if one of the threads it is dependent upon stops running. I've also generally taken the approach that exceptions should get propagated up to the thread routine and should cause the thread to exit, in a normal fashion, which would then allow the managing thread to notice one thread it is dependent upon is not running and shut down all relevant threads.

Exceptions in Real-Time Systems

Real-Time systems may be a bit of an overloaded term, but it is applicable to games. After all, what is a computer game other than a software-based real-time system? Otherwise, why would we complain about low frame rates?

My experience, however limited, with game programming and other real-time systems have led me to my current belief that real-time systems should not throw exceptions out to their clients. By this I mean a high level component, perhaps the main package interface, which may have a number of sub-components which make up the real-time system. These sub-components, particularly if well insulated from clients can throw exceptions to their heart's content, but the main engine control interface probably should not. Under what would be exceptional conditions it should shut down and indicate such an error condition to the client. It is fine if the client of that engine then used that error code to throw an exception based on it, but that decision has been pushed into application level code. This makes the engine more flexible for clients.

One problem with throwing exceptions is that there may be other real-time systems which are running concurrently and interdependently. This may cause one of them to not stop properly. Not to mention the issues involved with stopping a real-time system cleanly, particularly a multi-threaded one.

Exception Handling Philosophies

The fundamental reason for throwing an exception is to stop program execution along the current path. This generally means an error condition has occurred and normal program flow cannot continue. The best benefit to this is that client code can be simplified and yet still allows the implementer of a component to guarantee that if an error occurs the client will be aware of it. Basically, it removes the responsibility of a client to check error codes.

There seem to be three common reasons for throwing an exception:
  • A programming error - a pointer is null when it should not be
  • A resource is not available for acquisition or release - bad_alloc on memory exhaustion
  • A violation of a class invariant or a state of an object invariant - such as an invalid parameter is passed to a method or a calculation that goes out of range
So the reasons to throw an exception seem pretty straightforward. The more difficult question is where to handle exceptions. This is can be a lot more complicated an issue because of the flow control aspect of exceptions. We have to look at what most try/catch blocks are trying to accomplish.

Generally, I've seen try/catch blocks trying to do a few different things:
  • Prevent resource leaks
  • Stop an operation after it has failed
  • Stop an operation deciding how to continue based on the cause of the error
As we've seen from "Resource allocation is initialization" the first use of a try/catch block is completely unnecessary. We can eliminate the need for the try block, improving the clarity of the code, by using auto_ptr and similar classes to handle the releasing of resources in their destructors. This can also work for thread synchronization objects ( at least ones which you are not going to timeout on, such as a critical section ) and can be applied to other forms of resource acquisition. So, we can easily, relatively speaking, remove the try/catch blocks which are only preventing resource leaks and improve program performance and clarity.

The second use of a try/catch block is probably the most coherent use of exception handling. At some level in the system we attempt to perform a, usually, complex task which may fail in a number of ways. In this use of a try/catch block we are only concerned with complete success. No matter what the failure is we handle it in the exact same manner. Informing the user of the cause of the error can still be handled in a generic way, particularly if you have your own exception hierarchy. Your base class of your exceptions can provide a method for translating an exception into a human readable message that can be presented to the user. This allows the user to know why the operation failed and yet keeps the flow control simple in the error handler case.

The last example of exception handling seems at first like a simple permutation on the second one, but there are number of consequences involved. Having more than one catch block amounts is a form of branching and introduces more complex flow control. There are instances where this is unavoidable but hopefully you can minimize them. Maybe you handle only a few specific exceptions and the others are not handled. Perhaps these are from an unrelated exception hierarchy or you must handle specific exception classes in unique ways.

The critical question to consider is whether or not the system is handling the exception at the given location. Handling the exception can usually be read as not re-throwing it. If you are re-throwing an exception then you're not really handling it, you're just trying to return your object or system to a valid state. And if you find yourself having to do this in a number of different ways depending upon the exception thrown then you are potentially creating future maintenance problems. The reason for this is that it implies that proper error handling follows a chain of exception handlers that are spread across different levels of the system.

To minimize this complexity you should let the exceptions go up to the highest level possible in the system, usually the point at which the application, or highest level components of the package, initiated the operation.

Similarly, a thrown exception should always propagate outside of the function which generated the exception. They should also usually propagate out of the component which generated them. This follows the idea that exceptions are a non local form of error handling. If you throw an exception in within a function and handle it directly in that function then it is being used as a form of flow control and can obscure that flow.

Potential Pitfalls

Having run into a number of pitfalls in using exception handling, I'd like to offer some things to watch out for. Mainly these focus around understanding the system at run time and conceptualizing program flow control.

Knowing when to re-throw an exception and when not to can sometimes be difficult. It is far better to err on the side of caution and always re-throw the exception unless you are a thread routine or high-level component controlling a real-time system. This is at odds with deciding whether or not you are handling an exception and are not re-throwing it. The issue really hinges on the organization of your components and the level in the system at which they exist. Generally this means the top level components in the package or application should have most of the exception handlers.

The reason being is that if you do not re-throw the exception you are potentially introducing complex flow control that is not apparent when inspecting the code. Although using the technique of logging each exception on its construction can help alleviate this somewhat. This tends to indicate that you should minimize exception handlers in low-level components, using them only for freeing resources. When used this way, they can be replaced with objects on the stack which perform this resource management automatically.

Also, if you find yourself writing duplicate cleanup code in an exception handler and the regular function body, particularly if this is a catch(...) handler, it can made clearer using the such techniques as applying auto_ptr. Whenever this duplicate code exists it indicates such code is intended to execute under any condition of exit of the current scope; such as a destructor call for local objects on the stack.

A more thorny issue has to do with using multiple catch handlers for different types of exceptions. It is important to remember that each catch handler is a separate branch of execution. If you find yourself doing different things based upon the type of exception you catch you are walking down a potentially dangerous path. Basically, this amounts to two bad things, one is case analysis of object type, generally considered bad and using exceptions as a form of messaging.

The problem with case analysis is similar to other problems with case analysis. If a new exception object is introduced into the system it may need to have its own handler added and address the new 'case' of exception. Even worse is that if the catch handlers do radically different things then the application's behavior becomes dependent upon what exception it catches. This leads us into the second problem.

Since the program behavior is being guided by the type of exception caught a particular location you can get unexpected behavior caused by a different origin point of the exception. If the same exception is thrown from a different location in code that is called within the try block with the switch statement like catch handlers, then the program flow control will transfer to that handler for a different reason. This new reason may be just different enough from the origin set of assumptions that it causes subtle bugs. This can be particularly troublesome if this occurs in a low-level component that may not be accessible to the client.

Basically unless you write and maintain all the code that your try block executes you cannot safely make assumptions about the origin of an exception. Even if you do, the components are likely to be in different locations and their flow control interactions will not be immediately apparent. This can create a kind of pseudo-event handler mechanism and the problem, in this context, with the event handler model is you do not know the origin of the event, only the occurrence of it. It is far safer to assume that you have no idea what specific exceptions may be thrown from where, unless exception specifications are present, but there's that whole can of worms.

Unfortunately, oftentimes this form of exception type analysis is exactly the only viable solution. The thing to try to keep in mind is to avoid this as much as possible. And it can be mitigated a bit by trying to have those specific exception handlers at the highest level of organization possible. This is also true of re-throwing exceptions as well; let them bubble up as far as possible.

Conclusion

Exception handling is obviously a powerful feature of the C++ language. Although you can walk down the path of not using exceptions at al, their benefits far outweigh the costs. The key is to understand how exceptions operate; how they interact in a game application or other real-time system; where to use them and not use them; and to understand their effect on performance. Exceptions can greatly simplify the development of a C++ package and provide the component writer a way to enforce the assumptions made when the component was implemented.

I would be glad to hear of others experiences with exception handling. Just send me an email at melkior@mediaone.net

- Steve Crocker





Comments

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS