An Exceptional Quest

Published May 23, 2001 by Sobeit Void, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
[size="5"]Prelude

No, there aren't any damsels to save or dragons to fight here. Our insidious foe comes in different guises on different operating systems - General Protection Fault, Illegal Operation, Segmentation Fault...

Our defense: Exceptions.


This document presents error-handling strategies using exceptions and how to write robust code. Code that works is good, and code that doesn't should leave the system in the state it was before the program started. That is code robustness, and exceptions are designed exactly for such purposes.

Though C++ exceptions are platform independent, the discussion here tends towards the Windows platform.

If you feel a need to contact me, mail me at [email="robin@cyberversion.com"]robin@cyberversion.com[/email]


[size="5"]Why bother with Exceptions?

Take a look at this code fragment

int main()
{
Object *p = new Object(); // assume Object is a defined class

p->Some_Method();

delete p;

return 1;
}
At first glance, there is nothing wrong with the code. After all, most C++ books have code like that. However, the above code is not robust. What if the Some_Method() function generates an error that would crash the program? Then delete p would never be called and the memory would not be freed. This is a common situation known as a memory leak.

In C++, there isn't a garbage collector like other languages, and most people would say that is a serious flaw in C++. I used to think like that too, until I learned about exceptions.


[size="5"]What are Exceptions?

I will briefly cover what exceptions are here. I will not delve too deeply here since any C++ book will cover the syntax better than I can. This is meant to be a refresher course. Please consult a book for syntax details.

Exceptions are designed for error handling. There are two kinds of exceptions - hardware or software. The hardware exception occurs when you do a divide by zero, access invalid pointers, and so on. Dealing with hardware exceptions is usually platform dependent, and the C++ standard has no rules on how hardware exceptions are to be dealt with. This is where our discussion will be Windows dependent.

Example

int x = 3, y = 0;

int z = x / y; // this will cause a hardware divide by 0 exception
Hardware exceptions normally occur because of program bugs. We cannot really do much with them except handling them so we will leave this aside and focus on software exceptions first.

[size="3"]Try/catch/throw

Software exceptions are generated by the programmer using the keywords [font="Courier New"][color="#000080"]try[/color][/font], [font="Courier New"][color="#000080"]catch[/color][/font] and [font="Courier New"][color="#000080"]throw[/color][/font]. The try keyword is to guard a section of code against an exception; the catch keyword is to handle the exception; the throw keyword is to generate an exception.

Example

// any exception generated here will result in an illegal operation
// because it is not guarded by a try keyword

try
{
// all the code here is guarded against exceptions

throw 100; // generate an exception of type int

// Program flow will go to the catch (int) handler. If we throw another type of exception
// that we do not catch, say 'throw "String" ', then we would enter the catch(...) handler

// code here will not execute because an exception occurred above

}
catch( int )
{
// handle an exception of type int specifically
}
catch(...)
{
// handles all other kinds of exception we do not explicitly catch
}

// code resumes normally here
We can throw any type of exception - an int, a string, a class object. I generate an int exception here for simplicity. Later, we will generate a class object exception to identify the type of error.

The rule is we should catch whatever we throw.

[size="3"]How to try/catch

Therefore, the try/catch section is normally placed in the main section. This will ensure the program will never crash (well almost). When a function encounters an error situation that should end the program, it throws an exception.

Example

// Just some function, note we do not need a try/catch in the function.
void Function()
{
if ( 1 != 0 ) // fake some error situation
throw 100;

// this will never execute
}

int main()
{
try
{
// do some other code

Function();

// this will never execute

}
catch(...)
{
// display some error to the user

return 0; // return failure code to system
}

return l; // success
}
Note that there is only one try/catch in the main function. Functions in the program do not need try/catch blocks. You should note also that generating an exception would stop the program flow in the caller. So if a function calls a function that generates an exception, the code below the calling function would not execute.

Example

// A function that calls another function that generates an exception.
// The caller of this function would also have its program flow disrupted.
void First_Function()
{
Second_Function(); // if an exception is generated in the function, the
// exception would be propagated up to the caller
// of First_Function, until it reaches a try/catch.

// code here will not execute
}
This implies that exceptions should only be used to for serious errors and that the program should quit, hence the name Exceptions. Throwing an exception requires a catch handler so if the error is common, then a return code would be more appropriate.

The rule is to only throw exceptions for critical errors and limit your try/catch to the main program function only. Exceptions are errors but errors are not necessarily exceptions.

[size="3"]Rethrow

Sometimes you would like to catch the exception but still propagate the exception upwards. This is known as re-throwing the exception and is done with the throw keyword in a catch handler.

Example

void Function()
{
try
{
}
catch(...)
{
// catch the error

throw; // throw the exception upwards
}

}
This technique is normally used when the function needs to know about a specific type of exception but is not ready to handle the exception yet. I will show you how to use such a method later but you should avoid this technique if possible.

That should be about all you need to know about exception syntax. All of these should be readily available in a good C++ book.


[size="5"]Exception Safety and Error Handling

[size="3"]Scenario 1 - Exception Safety in Functions

This is where the fun part begins. What is exception safety? Well, it's code that is exception safe. Sort of a stupid answer, I know, so let's look at an example.

Example

void Function()
{
Object *p = new Object(); // assume Object defined

p->Some_Method();

delete p;
}
The above code is not exception safe because a memory leak may occur if an exception occurred between the memory allocation and de-allocation. Let's take a look at another example.

Example

void Function()
{
Object p; // assume Object defined

p.Some_Method();


}
The above code is exception safe because the destructor for p is always called regardless on the program flow. Stack objects are always de-allocated automatically when they go out of scope, so when an exception occurs, p will be cleaned up when the function exits.

Pointers are evil and should be replaced with stack objects.

However, if you keep following the rule above, you will soon run of the precious stack space. What if we do really need to keep the object on the heap using new? Let's also look at error handling strategies cause they coexist together.

Evil Goto

This is a classic way of error handling in C. It relies on functions to return a true/false for indicating success or failure.

I modified the example to include error-handling tactics as well. The below function is a bit more useful in the real world.

Example

int Function()
{
int bSuccess = 0; // flag for success, false initially.
Object *p = new Object(); // assume Object class defined

if (! p->Some_Method() ) // return encounter an error
goto Error:

// do some other code
bSuccess = true; // we finished without an error

:Error
delete p;

return bSuccess;
}
I bet you seen code like that before. Each function will return a value indicating success or failure. The caller will check the return value from calling other functions and it too will return a value indicating success or failure.

The above technique for error handling is bad because it requires the caller to check the return value of every call. The caller might not do that. You need to adopt the X-files philosophy - Trust no one. Besides, the function will become very messy when you have a lot more function calls.

Exceptions - A better solution?

Let's assume we do not use return values for error handling. Instead, we always throw an exception if we encounter an error. So the above example becomes

Example

void Function() // notice we do not need return values
{
Object *p = new Object();

p->Some_Method(); // Some_Method() can return void

if ( 1 != 0 ) // some fake error
throw 100; // if we encounter an error, always throw an
// exception like this
delete p;

}
Notice we do not require the functions to return a value indicating success or failure. Since an exception is generated when an error occurs, we know the program flow will be disrupted so the caller must always handle the error (or crash the program).

Of course the above code is not exception safe, so let's rewrite it.

Example

void Function() // note there is no return value
{
Object *p = NULL;

try
{
p = new Object();

p->Some_Method(); // will throw an exception if error

delete p;
}
catch(...)
{
delete p;
throw; // re-throw the exception to indicate error to caller
}
}
That is now exception safe but still looks messy. At the very least, we eliminated testing of return values when calling other functions. Can we do better? You bet!

SideTrack - SEH

I go into a small sidetrack here. VC++ has a set of keywords __try,__finally,__except known as Structured Exception Handling (SEH). It's basically error handling using exceptions for C programs.

The main limitation of SEH is that you are limited to using types that do not have destructors i.e. basic types and structs. However, you can mix functions that use SEH or C++ exceptions in your program.

Note that SEH is not portable but a VC++ specific implementation. Other compilers may support SEH but it is not guaranteed.

Example

void Function()
{
Object_Struct *p = NULL; // cannot have a destructor

__try // note the double underscore
{
p = new Object_Struct();

p->Some_Method();
}
__finally
{
delete p; // always executed when function exit

}
}
Code in the finally section is always executed when the function exits.

Just thought it's a nice trick to know when working with C code only. I will not go into further details on SEH. The MSDN docs have a nice section on SEH if you need to know more.

The Ultimate Solution - Smart Pointer

Let's recap where we are just in case you are lost. We want to use exceptions for error reporting instead of using return values. We also want to able to create heap objects but still have exception safety.

I said earlier about evil pointers and stack objects are always exception safe. This is the solution for our problem: a stack based pointer, commonly known as a smart pointer.

The main ideas behind smart pointers are to wrap the heap object in another stack based object (the smart pointer). The stack wrapper object is exception safe because it will be de-allocated when the function exits. Since it wraps the heap object, the heap object is cleaned up as well (in the smart pointer destructor).

The implementation of a smart pointer is actually quite advanced and requires the use of templates but all we need to know is that the destructor of the smart pointer will call delete on the heap pointer.

Let's look at it in action to understand more about it. I use a smart pointer implementation from the STL (Standard Template Library) known as the auto_ptr. Of course, you can always roll your own smart pointer classes.

Example

#include // include auto_ptr definition

void Function()
{
std::auto_ptr p( new Object() );

p->Some_Method();
}
That's it. The above code is exception safe and p is allocated on the heap. Contrast the elegance of this code with all the previous solutions.

Let's look at the smart pointer in more detail. Other than the ugly looking namespace syntax, notice that we declared a stack object of class auto_ptr. We then tell the smart pointer class the type of pointer is it meant to wrap.

std::auto_ptrWe then declare an instance of the smart pointer using a default constructor and use the smart pointer as if it was the original pointer. Isn't that neat??

This looks like a good time for some real code, so let's see the smart pointer in action. Take a look at the sample (SmartTest.zip) before continuing.

[size="2"]Smart Pointer Problems

Wait a minute. Aren't smart pointers the ultimate solutions? Yes, but there is a very big issue you must be aware of.

A smart pointer class calls delete on it's wrapped pointer, not delete[], so you cannot use it for arrays.

std::auto_ptr p( new Object[100] ); // BAD, NO ARRAYSTry to do the above and you are shooting yourself in the foot.

What happens if you need an exception safe array? Use the vector class.

std::vector v(100);

v[0] = 1; // use like an array
v[2] = 2;
What if you really need to use a pointer? This is mostly the case when you are working with legacy C code. Then, I'm sorry, but you have to resort to one of the messier solutions to ensure exception safety. After that, learn to really hate old C code.

If you need to know more about the STL and the auto_ptr, vector class etc. and how it can save you, go get a book. I recommend The C++ Standard Library by Nicolai M. Josuttis. No, I'm not getting paid for this, though I sure wish I were.

[size="3"]Scenario 2 - Exception Safety in classes & Resource wrapping

Now that we know that we should not be using raw pointers but always use smart pointers instead, let's look at how it should be done in classes

Example

class TestClass // A classic class
{
// sniped
private:
Object* p1; // assume Object class defined
Object* p2;
};
Using raw pointers for classes is not exception safe if your class uses more than one pointer because in the constructor, the allocation between the two pointers may fail.

Example

TestClass::TestClass() // Classic constructor
{
p1 = new Object();

// if some exception occurs here, then p1 is never freed and a memory leak occurs again
p2 = new Object();
}
The solution? Smart pointers to the rescue.

Example

class TestClass // exception safe class
{
public:

// Constructor
TestClass() : p1( new Object() ) // use initization list
{
// delay load the smart pointer
p2 = std::auto_ptr( new Object() );
}

private:
std::auto_ptr p1; // smart pointer
std::auto_ptr p2;
};
Now you think you are smart enough to take on the world, but let me tell you there is another problem with the above code.

The problem lies with the smart pointer.

Remember the problem of shallow and deep copy in classes? If you have raw pointers, you need to define the copy constructor and assignment operator or only the pointers will be copied due to the default implementation of copy constructor and assignment operator doing bitwise copying.

We have a similar problem with the auto_ptr.

Ownership of the resource

The auto_ptr specifies that there can be only one owner on the resource it wraps at any one time. If you do an assignment, the ownership is transferred and delete will not be called on the original auto_ptr.

Example

std::auto_ptr p( new Object() );
std::auto_ptr q;

q = p; // p give ownership of resource to q

// when both go out of scope, only q destructor calls delete.
What this means is with respect to copying, the auto_ptr does not cut it. If you create more than one object from the class, you cannot perform any copying operations between the objects.

Frankly, the auto_ptr is a very weak implementation of a smart pointer. Designing a safe smart pointer class has given the C++ Standard committee tons of headaches. They probably ran out of time to design an all around useful smart pointer class (they have deadlines too), so we are stuck with auto_ptr.

What's the solution then? Well, you can either write your own or visit www.boost.org for a set of smart pointer classes that allow copying. You can then use these smart pointers safely in classes

Resource wrapper

Let's give a more realistic example. I will use the GetDC function from the Windows platform.

If you make a call to GetDC, the MSDN documentation states that the ReleaseDC function must be called to release the device context.

Now that you are getting paranoid about exception safety, you know that a potential leak will occur if the code between GetDC and ReleaseDC throws an exception.

What we need to do is to again make sure the GetDC/ReleaseDC calls are wrapped in a stack based wrapper class. A smart pointer cannot save us now, because the smart pointer only uses new and delete.

If you code in Windows or use any library, you usually come across an initialization and release resource function. These two functions are analogous to new and delete. They need to be wrapped up in a resource wrapper class like the smart pointer to ensure exception safety.

Unfortunately, the solution is to roll your own resource wrappers or use one from MFC. Personally, I would avoid MFC.


[size="5"]Exception class

[size="3"]The Base

Up to now, I have been throwing an int exception. As I said before, you can use throw any type of exception. So let's define a more useful error reporting class.

Example

class BaseException
{
public:
BaseException( const char * const err )
{
strncpy( buf, err, 128 );
}

virtual const char * what() // get the error string
{
return buf;
}

protected:
char buf[128]; // store the error string
};
I use a char buffer for simplicity, but I recommend using the STL string class to replace all the strings in your programs.

So now instead of throwing an int, we throw a BaseException object and pass a meaningful error string to the constructor. Later in the catch handler, we can retrieve the error string using the what() function.

Example

try
{
}
catch( BaseException &e ) // note we catch by reference
{
cout << e.what() << endl;
}
Getting exceptions to work in Windows is the same as in a console program. Just wrap the main message pump function with a try/catch block.

[size="3"]What can we do with it?

Now we have a base exception class, we can use inheritance to create specific types of exceptions. Each derived class can correspond to a different type of error - File not found, Not enough memory, etc.

I will give two examples here - one for dealing with Windows errors and the other for handling SEH exceptions.

[size="3"]Windows exception class

I bet you have noticed Window functions use return codes to signal errors and GetLastError() to find out what the error was. Since we are coding in Windows, we have to adapt to their weak error handling implementation.

You can see the WindowsException class in the sample later on. It just translates the GetLastError code to a system message. It's pretty simple to understand so you can just look at the source.

[size="3"]SEH Exception class

SEH in VC++ encompasses hardware exceptions too, but I bet you figured that out already. In any case, Microsoft 'suggests' C++ exception handling to be used over SEH, and has kindly provided us with a means to translate SEH exceptions to C++ exceptions.

The function we are interested in is _set_se_translator. Whenever a hardware exception occurs, the registered translator function will be called. So what we do is throw a C++ exception within the translator function, like so

Example

// the translator function
void SEH_To_C++( unsigned int u, EXCEPTION_POINTERS *exp )
{
throw int; // throw an exception of type int
}


void main()
{
try
{
_set_se_translator( SEH_To_C++ ); // register the translator

// now all hardware exceptions will generate an C++ int
// exception
}
catch(...) {}
}
Note: _set_se_translator must be called for each separate thread.

[size="3"]Sample

Combining all your newfound knowledge, I present a simple sample (Exception.zip) with the exception class in action. You will see how we can incorporate and extend the base exception in the Windows environment.

I don't normally use MFC, but since I'm not getting paid for this, oh well, too bad for the code bloat. Don't blame me; I'm not the one who wrote the wizard.

The main points to note are
  1. The main file you should look at is BaseException.h. It contains the aforementioned exception classes.
  2. The entire main function is enclosed in a try/catch block, just like a console program.
  3. I generate an exception when the mouse is moved a few times when the program is active. Of course the rest of the MFC Appwizard generated code is not exception safe, but I'm too lazy to fix it.
[size="3"]More! I want more!

I know you want more, so here it is

More information

You can store the line and source file where the exception occurred by passing in two additional parameters. Use the __FILE__, __LINE__ macros.

You can also do a stackwalk inside the exception class too. Take a look at the BugSlayer column by John Robbins at MSDN on how to do this.

Inherit from STL exception class

Instead of defining a new class by itself, your base exception class can be inherited from the STL exception class. This would allow you to catch exceptions from the standard library as well as your exceptions using one catch handler.

Evil Assert

Assertions are evil. No wait, I don't mean there are not useful, I meant the default assert implementation just calls abort, and that may not be exception safe.

I recommend throwing an exception instead when an assertion failure occurs. You can do this easily by extending the base exception class.

Extending it for DirectX

Many times, I see ingenious hacks being invented for error handling in DirectX. Personally, I'm guilty of those hacks too.

So, this is the latest hack using exceptions for a full screen DirectX application.
  1. Extend another class that takes in a HRESULT. In that class, translate the passed result to a string using either the D3DX library or manually translating the error code. This is similar to the classes I presented.
  2. In the catch handler of the main function, restore the video mode before displaying the MessageBox. This will ensure the Messagebox is always visible without a need to call FlipToGDI at all (which is a slow call).
  3. If the DirectX function call fails, throw an exception.
If you have worked in full screen DirectX applications before, you will probably realize a MessageBox does not always show up. This is precisely the reason why I throw an exception for all the errors, even for assertion failures. When the exception reaches up to the main function, I restore the display mode and then I display the MessageBox describing the error.

Anyway, the above 'hack' is for DirectX 7.0. I haven't worked with DirectX 8 yet so I cannot guarantee if there are more 'hacks' needed.


[size="5"]Final Thoughts - Will this guy ever stop?

Yeah, yeah, soon enough. Let's go through some more details before I sign off.
[size="3"][bquote]1. Destructors cannot throw.
[bquote]Destructors are meant for cleaning up and should never throw. If you find you need to throw in a destructor, it's best to redesign the class[/bquote][size="3"]2. No setjmp/longjmp/goto allowed.
[bquote]This is probably for the better. If don't know what I'm talking about, good for you and move along.[/bquote][size="3"]3. Asynchronous Vs Synchronous exception handling.
[bquote]Under asynchronous EH (exception handling), the compiler assumes that any line can throw an exception. Under synchronous EH, the compiler only assumes exceptions occurred where the programmer specify the throw.

What this means is that synchronous EH is more efficient for functions that don't throw exceptions, since the compiler don't generate code to keep track of the unwinding of the stack

Maybe I'm paranoid, but I like to ensure that even my release programs are exception safe, so I prefer async EH to sync EH.

As far as I can tell, sync EH might not be exception safe when a hardware exception occurs. If someone knows more about this, please enlighten me.

In VC++ 5.0, async EH was the default. In VC++ 6.0, sync EH is the default.

So if you want to enable async EH, look at the /EHa compiler flag.[/bquote][size="3"]4. Performance of exceptions.
[bquote]Setting up a try/catch block does take some resources. In general, try to reduce the number of try/catch blocks. Exceptions are errors but errors are not necessarily exceptions. So if the error is a very predictable type of error, maybe a return code is more appropriate.[/bquote][size="3"]5. Threads
[bquote]Each thread requires its own try/catch block. This would mean exceptions cannot propagate between threads. What this means is that the main thread needs some way to be notified when a spawned thread encounters an exception. I don't know, maybe your main thread needs to monitor the state of all the threads continuously, and if any thread encounters an exception, the main thread tells all other threads to end and throws an exception.

I have not found of an elegant solution with respect to threading yet. If you do have some tips, please share.[/bquote][size="3"]6. Static Objects
[bquote]Since exceptions thrown require a try/catch block to handle the exception, a static object constructor should not throw. If you must throw, you probably have to set the unexpected handler in the first line of the constructor.

Again, I have not found an elegant solution to this problem.[/bquote][size="3"]7. Windows Quirks
[bquote]No doubt when coding with Windows and its old C code, you will come across many situations requiring weird hacks.

For example, in the WM_INITDIALOG message handler in a dialog wndproc, you need a try/catch block to make a call to EndDialog in case an exception occurs while creating the dialog.

Also, when you display a MessageBox in the catch handler, you cannot pass it a NULL window handle or the program will crash when exceptions are thrown from the wndproc (certain messages).

Just be on the lookout for weird stuff like this.[/bquote][/bquote]I hope I have presented some useful information about exceptions and how to write safe code. Now, please don't write any programs that crash my system.
Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

An in depth discussion of C++ exceptions, and how to write exception-safe code when using them.

Advertisement
Advertisement
Advertisement