Making a Constructor Exception-Safe
Problem
Your constructor needs to uphold basic and strong exception-safety guarantees. See the discussion that follows for the definitions of "basic" and "strong" guarantees.
Solution
Use try and catch in the constructor to clean up properly if an exception is thrown during construction. Example 9-2 presents examples of the simple Device and Broker classes. Broker constructs two Device objects on the heap, but needs to be able to properly clean them up if an exception is thrown during construction.
Example 9-2. An exception-safe constructor
#include #include using namespace std; class Device { public: Device(int devno) { if (devno == 2) throw runtime_error("Big problem"); } ~Device( ) {} }; class Broker { public: Broker (int devno1, int devno2) : dev1_(NULL), dev2_(NULL) { try { dev1_ = new Device(devno1); // Enclose the creation of heap dev2_ = new Device(devno2); // objects in a try block... } catch (...) { delete dev1_; // ...clean up and rethrow if throw; // something goes wrong. } } ~Broker( ) { delete dev1_; delete dev2_; } private: Broker( ); Device* dev1_; Device* dev2_; }; int main( ) { try { Broker b(1, 2); } catch(exception& e) { cerr << "Exception: " << e.what( ) << endl; } }
Discussion
To say that a constructor, member function, destructor, or anything else is "exception-safe" is to guarantee that it won't leak resources and possibly that it won't leave its object in an inconsistent state. In C++, these two kinds of guarantees have been given the names basic and strong.
The basic exception-safety guarantee, which is quite intuitive, says that if an exception is thrown, the current operation won't leak resources and the objects involved in the operation will still be usable (meaning you can call other member functions and destroy the object, i.e., it won't be in a corrupt state). It also means the program will be left in a consistent state, although it might not be a predictable state. The rules are straightforward: if an exception is thrown anywhere in the body of (for example) a member function, heap objects are not orphaned and the objects involved in the operation can be destroyed or reset by the caller. The other guarantee, called the strong exception-safety guarantee, ensures that the object state remains unchanged if the operation fails. The latter applies to postconstruction operations on an object, since, by definition, an object that throws an exception during construction is never fully constructed and therefore never in a valid state. I will return to the subject of member functions in Recipe 9.4. For now, let's focus on construction.
Example 9-2 defines two classes, Device and Broker, that don't do much, but could easily represent any sort of device/broker scenario where you have some class that opens a connection to each of two devices and manages communication between them. A broker is useless if only one of the devices is available, so you want transactional semantics when you instantiate a broker, such that if one of the two throws an exception when it is being acquired, the other is released. This will ensure memory and other resources are not leaked.
TRy and catch will do the job. In the constructor, wrap the allocation of heap objects in a try block and catch anything that is thrown during their construction like this:
try { dev1_ = new Device(devno1); dev2_ = new Device(devno2); } catch (...) { delete dev1_; throw; }
The ellipsis in the catch handler means that anything that is thrown will be caught. This is what you need here, because all you're doing is cleaning up after yourself if something goes wrong, then rethrowing regardless of what sort of exception was thrown. You need to rethrow so the client code that is trying to instantiate the Broker object can do something useful with the exception, like write its error message somewhere.
I only delete dev1_ in the catch handler because the last chance for an exception to be thrown is in the call to new for dev2_. If this throws an exception, than dev2_ will not be assigned a value and, therefore, I don't need to delete it. However, if you do something after dev2_'s initialization, you will need to be sure to clean it up. For example:
try { dev1_ = new Device(devno1); dev2_ = new Device(devno2); foo_ = new MyClass( ); // Might throw } catch (...) { delete dev1_; delete dev2_; throw; }
In this case, you don't need to worry about deleting pointers that were never assigned real values (as long as you properly initialized them in the first place), since deleting a NULL pointer has no effect. In other words, if the assignment to dev1_ throws an exception, your catch handler still calls delete dev2_, but that's okay as long as you initialized it to NULL in the initializer list.
As I said in Recipe 9.1, designing a sound, flexible exception strategy can be tricky, and exception-safety is no different. For a detailed look at designing exception-safe code, see Exceptional C++ by Herb Sutter (Addison Wesley).
See Also
Recipe 9.3