Introduction
It is always a dream for every programmer under the sun, to write programs that never smash. So this article presents some guidelines in trapping exceptions which can kill applications, and thereby ensuring the graceful exit of a program. I have included some code snippets that show how to trap exceptions.
Guidelines for writing C++ applications
Coping with object creation and destruction
-
Whenever you create an object in the heap (using the new operator), always remember to delete the same (using the delete operator).
You can create an object of type T
dynamically by using a new
expression such as:
T* obj = new T();
The original C++ rule says, new
operator always returns a NULL
pointer if memory could not be allocated. So it�s a good practice to check for NULL
after creating the object, and before using it.
When you want to deallocate a heap object, you can use the delete
expression such as:
delete obj;
Before deleting an object, always check for NULL
. (This is because, may be you are trying to delete an object that is already deleted.)
T* obj = new T();
if( obj != NULL )
{
}
{
if( obj != NULL )
delete obj;
}
All the member variables of a class should be created and initialized in the constructor itself. Most of the cases its not possible, so always ensure that objects are used only after they are initialized.
-
When creating an array of objects, you must have a constructor in place, that can be invoked without arguments.
T* objs = new T[N];
To deallocate the array, you must use this form of delete
expression:
delete [] objs;
-
Practice the habit of declaring virtual destructors in base classes.
When you try to delete an object of a derived class through a pointer to a base class, only the base class destructor is executed, leading to all kinds of problems, like memory and other resource leaks.
class A {
. . .
};
class B : public A {
. . .
};
A* pB = new B;
delete pB;
A common rule is that if a class has a virtual function, it probably needs a virtual destructor as well�and once we decide to pay the overhead of a vtable pointer, subsequent virtual functions will not increase the size of the object. So, in such a case, adding a virtual destructor doesn't add any significant overhead. So it�s always nice to have a virtual destructor in base classes.
Using Exceptions
When I started to use exceptions, it rapidly became evident that exceptions are more difficult to use effectively than it first seems. In fact, the more I explored how exceptions can interact with non-exception handling code, the more convinced I became that exceptions may be the most difficult feature of C++ to use. So there are some guidelines, which you should follow while using exceptions.
-
When you propagate an exception, try to leave the object in the state it had when the function was entered.
This is the Golden Rule of exception handling. It is not going to do much good for a program to handle an exception if the program has gone into an invalid state as a result of the exception propagating to the point where it could be handled. I personally feel that writing code that meets this goal is very difficult. I put this guideline first because it is the target that should always be the ultimate goal. I will only give a few simple suggestions here.
- Make sure your
const
functions really are const
.
- Perform exception prone operations early.
- Avoid side effects in expressions that might propagate exceptions.
try {
stack[esp++] = element;
} catch (...) {
--esp;
}
-
If you cannot leave the object in the same state it had when the function was entered, try to leave it in a good state.
The reason why this is required is that the client might not be paying as much attention to exception handling, as they should. Client might handle the exception, but not realize that the object is no longer valid. In such cases, always make sure that any further attempt to use the object will be rejected.
-
If you cannot leave the object in a "good" state, make sure the destructor will still work.
If Guideline 1 and 2 are not possible, as a last resort, try to leave the object in such a state that it can always be safely destroyed. Never forget that as an exception propagates, it will unwind the stack frame of every function it propagates through. Many times, this will invoke the destructor of the very object that threw the exception.
-
Avoid resource leaks.
The most obvious type of resource leak is a memory leak, but memory is not the only resource that can leak. In one sense, a resource leak is just another example of an inconsistent state. To avoid memory leaks, always try to use auto_ptr<>
template class provided by the Standard C++ Library. There are three different instances where exceptions can cause resource leaks: in a constructor, in a destructor, and in a function (whether a member of a class or not).
When an exception propagates from a constructor, the partial object that has been constructed so far is destroyed. If necessary, any memory allocated from the free store for the object is also released. If, during the construction, a resource (such as memory) is obtained directly and not as part of a sub-object whose destructor will release the resource, then a resource leak can occur. This situation can be dealt with using try
block to catch the exception and attempting to release the memory. So initialize the pointers to NULL
, and delete them all in the catch
block.
When we have resource leaks in destructors: As a general rule, we do not want to throw (or propagate) exceptions from destructors. Nevertheless, we cannot always prevent it. In situations like this, try using auto_ptr<>
, and transfer ownership of the resources to the temporary objects. When the destructor body exits, these objects are destroyed, deleting their pointers.
-
Do not catch any exception you do not have to.
Actually there is an old C rule of error handling, that states: "do not test for any error condition you do not know how to handle�. In C++ it means, it is a waste of time (yours and the computers) to catch an exception you do not know how to handle. I can give a few suggestions on how to behave on a situation like this.
- Always use a
catch(...)
block to cope with propagating exceptions.
- Do not "handle" any exception that cannot be "fixed."
- Do not throw exceptions from a destructor body.
- If you get stuck, call
terminate()
or exit()
.
Before exceptions, the normal way to abnormally terminate a program was by calling the C library function abort()
. In the new C++ world, we should call terminate()
instead. terminate()
just calls terminate_handler
. In the default case, this calls abort()
, but the user can replace the default terminate_handler
with a program specific version. So call terminate()
to allow any user defined terminate_handler
to run.
-
Do not hide exception information from other parts of the program that might need it.
The purpose of exceptions is to pass information from the point where an error is detectable to a point where the error can be handled. If you throw a different exception rather than re-throwing the original exception, you want to make sure you are increasing the possibility of the exception being handled, by doing so.
- Always re-throw the exception caught in a
catch (...)
clause.
- Re-throw a different exception only to increase the level of abstraction or capability.
- Make sure one
catch
block does not hide another.
-
Unless you have a Strong Guarantee of exception safety (Guideline 1), assume you must destroy the object after any exception.
When you handle an exception, you correct whatever was wrong and then redo the failed operation. In order for this to work, you have to be sure that the operations which failed left their objects in the state they had before the operations were attempted. This is Guideline 1, and is what the C++ Standard calls the "Strong Guarantee". If you cannot say for certain that Guideline 1 is being followed, then all bets are off. If Guideline 2 has been followed, you might be in a position to reuse the object, but in general the only really safe thing to do is destroy the object and start over.
-
Always catch an exception by reference.
This doesn�t require much clarification, because I assume that everyone already knows this.
I presume, I have gone a bit detail into exceptions, so I am concluding this topic of exception-handling with a few words. �Exception handling is obviously a powerful feature of the C++ language. Although you can walk down the path of not using exceptions at all, their benefits far outweigh the costs�.
A little bit UNIX flavor
The UNIX operating system uses signals as a means of notifying a process that some event, often unrelated to the process's current activity, has occurred that requires the process' attention. Signals are delivered to a process asynchronously; a process cannot predict when a signal might arrive. Failing to properly handle various signals would likely cause your application to terminate, when it receives such signals. When we say that "Signals are being handled", we mean that our program is ready to handle such signals that the operating system might be sending it (such as signals notifying that the user asked to terminate it, or that a network connection we tried writing into, was closed, etc.). A typical signal handler would look like this:
void catch_int(int sig_num)
{
signal(SIGINT, catch_int);
fflush( stdout );
cout <<"some message"<<endl;
}
Now a few thumb rules for writing a signal handler.
- Make it short - the signal handler should be a short function that returns quickly. Instead of doing complex operations inside the signal handler, it is better that the function will raise a flag (e.g. a global variable, although these are evil by themselves) and have the main program check that flag occasionally.
- Proper Signal Masking - don't be too lazy to define proper signal masking for a signal handler, preferably using the
sigaction()
system call. It takes a little more effort than just using the signal()
system call, but it'll help you sleep better at night, knowing that you haven't left an extra place for race conditions to occur.
- Careful with "fault" signals - If you catch signals that indicate a program bug (
SIGBUS
, SIGSEGV
, SIGFPE
), don't try to be too smart and let the program continue, unless you know exactly what you are doing (which is a very rare case) - just do the minimal required cleanup, and exit, preferably with a core dump (using the abort()
function).
- Careful with timers - when you use timers, remember that you can only use one timer at a time, unless you also use the
VTALRM
signal. If you need to have more than one timer active at a time, don't use signals, or devise a set of functions that will allow you to have several virtual timers using a delta list of some sort.
- Signals are NOT an event driven framework - it is easy to get carried away and try turning the signals system into an event-driven driver for a program, but signal-handling functions were not meant for that. If you need such a thing, use some framework that is more suitable for the application.
In Windows, also we can trap for many different exceptions such as ACCESS_VIOLATION
, STACK_OVERFLOW
, ARRAY_BOUNDS_EXCEEDED
, DATATYPE_MISALIGNMENT
, FLT_DENORMAL_OPERAN
, FLT_DIVIDE_BY_ZERO
, FLT_INEXACT_RESULT
, FLT_INVALID_OPERATION
, FLT_OVERFLOW
, FLT_STACK_CHECK
, FLT_UNDERFLOW
, INT_DIVIDE_BY_ZERO
, and INT_OVERFLOW
using the try
- except
blocks.
Using DEBUG functions
-
Using Assert
It is not uncommon for bugs to linger because a section of code is not behaving as you think it is. I've stared at a line of code for minutes on end before I've been able to find a simple typo. More complicated bugs can be murder to find because, it often turns out, I'm assuming I see something that isn't there. One of the most powerful techniques for overcoming this problem is the Assert
macro.
Many compilers offer an Assert()
macro as part of their default library. The Assert()
macro is designed to assert a fact about your program-that is, to document (and test) what you think is true at a given moment in the history of your program. Here's how it works: You assert something by passing it as an argument to the Assert
macro. The macro takes no action if you are correct, but it aborts your program and puts up an error message if you are not correct-if the asserted "fact" is not true.
For example,
Assert( x > 10 )
The Assert
macro is a powerful debugging tool, but for it to be acceptable in a professional development environment, it must not create a performance penalty nor increase the size of the executable version of the program. To accomplish this, the preprocessor collapses the Assert
macro into no code at all if Debug
is not defined. Thus, in your development environment, you can use Assert
to find your bugs and misunderstandings, but when your code ships, there is no penalty.
-
Using Class Invariants
Most classes have some conditions that should always be true. For example, it may be true that your Circle
object should never have a radius of zero, or that your Animal
should always have an age greater than 0 and less than 100. It can be very helpful to declare an Invariants()
method that returns true only if all if these conditions are true. You can then Assert(Invariants())
at the start and completion of every class method.
Conclusion
"A robust program is resistant to errors -- it either works correctly, or it does not work at all; whereas a fault tolerant program must actually recover from errors".
Credits
While working on this, I have referred the following articles:
History
- Date submitted: 05 September 2003.