Author : Steve Crocker
Page : << Previous 2 Next >>
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
Page : << Previous 2 Next >>