Concepts & Techniques
Rule Two:
Use features of the language to help
The most helpful language feature is the fact that all locally constructed objects will be destroyed when a function returns (or a block - delimited by {} - completes) whether an exception is thrown or not. This means that, if an exception is thrown from deep within a set of function calls, the destructors will be called for every object created within those functions.
This allows the use of RAII (Resource Acquisition Is Initialisation) techniques. Essentially, this means that resources used by an object (memory, file handles, mutexes, etc) are acquired by constructors, and cleaned up by destructors. As the destructors will be called as control passes from a throw statement to the first matching catch clause, this means that all resources grabbed in between will be cleaned up. While member functions may be used to reinitialise the resource (eg a class working with an array may dynamically resize the array), those functions must ensure that they do not cause a leak, and ensure that the destructor will have something valid to clean up.
The language also guarantees that, if an exception is thrown while an object is being constructed, that any parts of the objects successfully constructed will be destroyed. This can be used to avoid resource leaks. For example;
class Cat {}; // definition of Cat is incidental to this example
class Base
{
public:
Base() : x(new Cat) {};
virtual ~Base() {delete x;};
private:
Cat *x;
};
class Dog {};
class Derived : public Base
{
public:
Derived(): Base(), y(new Dog) {throw Foo;};
~Derived() {delete y;}
private:
Dog *y;
}
The language guarantees that, when creating a Derived, the base classes are
constructed first, and then data members are constructed. In this example, Base
is constructed, then y is initialised. The constructor of Derived throws an
exception. The language then guarantees that the components of Derived that
have been successfully initialied will be cleaned up, in reverse order of their
construction. So, in this example, y will be deleted, and the destructor of
Base will be invoked. This approach means that, if construction of an object
fails, the object never exists (and an exception is encountered).
This rule allows one to avoid the common, but naive, "two phase construction" approach,
which essentially means that a constructor sets the object into some default "safe
state", and then requires calling of something like an init() function to initialise
the object into the needed state. The problems with such an approach include
the possibility of forgetting to call the init() function, the possibility of
calling it more than, and (often) the need for every member function to check
that the object is not in the "default" state before using it. These problems
increase likelihood of errors, and make the class implementation more difficult
to understand and maintain. The most usual reason for "two phase construction" is
avoiding grabbing resources before they are needed. This can also be avoided
by not constructing the whole object until it is needed.
This practice also simplifies the provision of multiple functions by one class.
In this example, class Base manages an instance of a Cat, so the implementer
of class Derived need not worry about the creation and destruction of that Cat.