Exception Safety and Error HandlingScenario 1 - Exception Safety in FunctionsThis 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 GotoThis 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 - SEHI 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 PointerLet’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 <memory> // include auto_ptr definition void Function() { std::auto_ptr<Object> 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_ptr<Class Type> We 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 before continuing. Smart Pointer ProblemsWait 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<Object> p( new Object[100] ); // BAD, NO ARRAYS Try 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<Object> 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. Scenario 2 - Exception Safety in classes & Resource wrappingNow 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<Object>( new Object() ); } private: std::auto_ptr<Object> p1; // smart pointer std::auto_ptr<Object> 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 resourceThe 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<Object> p( new Object() ); std::auto_ptr<Object> 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 wrapperLet’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. |