Topic : C++ Exceptions Handling
Author : Steve Crocker
Page : << Previous 3  
Go to page :


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 other's experiences with exception handling. Just send me an email at melkior@mediaone.net

- Steve Crocker

Page : << Previous 3