Making an Initializer List Exception-Safe

Problem

You have to initialize your data members in the constructor's initializer list, and, therefore, cannot use the approach described in Recipe 9.2.

Solution

Use a special syntax for try and catch that catches exceptions thrown in the initializer list. Example 9-3 shows how.

Example 9-3. Handling exceptions in an initializer

#include #include using namespace std; // Some device class Device { public: Device(int devno) { if (devno == 2) throw runtime_error("Big problem"); } ~Device( ) {} private: Device( ); }; class Broker { public: Broker (int devno1, int devno2) try : dev1_(Device(devno1)), // Create these in the initializer dev2_(Device(devno2)) {} // list. catch (...) { throw; // Log the message or translate the error here (see // the discussion) } ~Broker( ) {} private: Broker( ); Device dev1_; Device dev2_; }; int main( ) { try { Broker b(1, 2); } catch(exception& e) { cerr << "Exception: " << e.what( ) << endl; } }

 

Discussion

The syntax for handling exceptions in initializers looks a little different from the traditional C++ syntax because it uses the try block as the constructor body. The critical part of Example 9-3 is the Broker constructor:

Broker (int devno1, int devno2) // Constructor header is the same try : // Same idea as a try {...} block dev1_(Device(devno1)), // The initializers follow dev2_(Device(devno2)) { // This is the constructor body. } catch (...) { // The catch handler is *after* throw; // the constructor body }

TRy and catch behave as you would expect; the only difference from the usual syntax of a TRy block is that when you want to catch exceptions thrown in an initializer list, TRy is followed by a colon, then the initializer list, and then the TRy block, which is also the body of the constructor. If anything is thrown in either the initializer list or the constructor body, the catch handler that follows the constructor body will get it. You can still embed additional try/catch pairs in the body of the constructor if you have to, but nested TRy/catch blocks usually get ugly.

In addition to moving the member initialization to the initializer list, Example 9-3 is different from Example 9-2 for another reason. The Device object members aren't created on the heap this time with new. I did this to illustrate a couple of points regarding safety and member objects.

First, using stack instead of heap objects lets the compiler provide its built-in safety. If any of the objects in the initializer list throws an exception during construction, its memory is deallocated automatically as the stack unwinds in the exception-handling process. Second, and even better, any other objects that have already been successfully constructed are destroyed without you having to catch the exception and delete them explicitly.

But maybe you require or prefer heap members. Consider an approach like the original Broker class in Example 9-2. You can just initialize your pointers in the initializer list, right?

class BrokerBad { public: BrokerBad (int devno1, int devno2) try : dev1_(new Device(devno1)), // Create heap objects with dev2_(new Device(devno2)) {} // initializers catch (...) { if (dev1_) { delete dev1_; // Shouldn't compile, and delete dev2_; // is a bad approach if it } // does throw; // Rethrow the same exception } ~BrokerBad( ) { delete dev1_; delete dev2_; } private: BrokerBad( ); Device* dev1_; Device* dev2_; };

No. There are two problems here. To begin with, this should not be allowed by your compiler because the catch block of a constructor should not allow program code to access member variablesat that point, they don't exist. Second, even if your compiler permits it, it is a bad idea. Consider the case where the construction of dev1_'s object throws an exception. This is the code that will be executed in the catch handler:

catch (...) { if (dev1_) { // What value does this contain? delete dev1_; // Now you are deleting an undefined value delete dev2_; } throw; // Rethrow the same exception }

If an exception is thrown during the construction of dev1_, then new doesn't get a chance to return the address to the newly allocated memory and dev1_ is unchanged. Then what does it contain? It's undefined, because it was never initialized with a value. As a result, when you call delete dev1_, you will probably be deleting a garbage pointer address, which means your program will crash, you will get fired, and you will have to live with that shame for the rest of your life.

To avoid such a life-altering fiasco, initialize your pointers to NULL in the initializer list, and then create the heap objects in the constructor. This way it's easy to catch anything that goes wrong and clean up the mess, since calling delete on NULL pointers is okay.

BrokerBetter (int devno1, int devno2) : dev1_(NULL), dev2_(NULL) { try { dev1_ = new Device(devno1); dev2_ = new Device(devno2); } catch (...) { delete dev1_; // This will always be valid throw; } }

So, to summarize, if you must use pointer members, initialize them to NULL in the initializer list, then allocate their objects in the constructor using a try/catch block. You can deallocate any memory in the catch handler. However, if you can work with automatic members, construct them in the initializer list and use the special try/catch syntax to deal with any exceptions.

See Also

Recipe 9.2

Категории