Chapter 11. Avoid Writing Code in Destructors

In the previous chapter, we discussed why you should try to avoid writing copy constructors and assignment operators at all. In this chapter we discuss why you should avoid writing code in the destructor. I am not saying that the destructor method should not exist, just that if you do write one, it’s a good idea to design your class so that the destructor is empty. The following is acceptable:

virtual ~MyClass() {}

We will use the term an empty destructor when talking about a destructor that has no code inside the curly brackets.

There are several reasons why you might need to write a destructor:

  • In a base class, you might want to declare it virtual, so that you can use a pointer to the base class to point to an instance of a derived class.

  • In a derived class, you do not have to declare it virtual, but might like to do so for the sake of readability.

  • You might need to declare that the destructor does not throw any exceptions.

Let’s consider the last reason more closely. It is widely accepted in the C++ literature that throwing exceptions from a destructor is a bad idea. This is because destructors are often called when an exception is already thrown, and throwing a second one during this process would lead to the termination (or crash) of your program, which is probably not your intention. Therefore, in some classes, destructors are declared as follows (this example comes from the file scpp_assert.hpp):

virtual ~ScppAssertFailedException() throw () {}

which means that we promise not to throw an exception from this destructor.

So you can see that it is necessary from time to time to write a destructor. Now let us discuss why it should be an empty one. When would you need any non-trivial code in the destructor? Only if you have acquired, in the constructor or some other method of your class, some resource that you need to release when the object goes away, such as in the following example:

class PersonDescription {
 public:
  PersonDescription(const char* first_name, const char* last_name)
  : first_name_(NULL), last_name_(NULL) {
    if(first_name != NULL)
      first_name_ = new string(first_name);

    if(last_name != NULL)
      last_name_ = new string(last_name);
  }

  ~PersonDescription() {
    delete first_name_;
    delete last_name_;
  }


 private:
  PersonDescription(const PersonDescription&);
  PersonDescription& operator=(const PersonDescription&);

  string* first_name_;
  string* last_name_;
};

The design of this class violates everything we have discussed in earlier chapters. First of all, we see that every time we might need to add a new element of a person’s description, such as a middle name, we would need to remember to add a corresponding cleanup to the destructor, which is a violation of our “do not force the programmer to remember things” principle. A much better design would be:

class PersonDescription {
public:
  PersonDescription(const char* first_name, const char* last_name) {
    if(first_name != NULL)
      first_name_ = new string(first_name);

    if(last_name != NULL)
      last_name_ = new string(last_name);
  }

private:
  PersonDescription(const PersonDescription&);
  PersonDescription& operator=(const PersonDescription&);

  scpp::ScopedPtr<string> first_name_;
  scpp::ScopedPtr<string> last_name_;
};

In this case, we don’t need to write a destructor at all because the one generated for us automatically by the compiler will do the job, and this leads to less fragile code while doing less work. However, this is not the main reason for choosing this second type of design. There are more serious potential hazards in the case of the first example. Suppose we have decided to add sanity checks that the caller has provided the first name and last name:

class PersonDescription {
public:
  PersonDescription(const char* first_name, const char* last_name)
  : first_name_(NULL), last_name_(NULL) {
    SCPP_ASSERT(first_name != NULL, "First name must be provided");
    first_name_ = new string(first_name);

    SCPP_ASSERT(last_name != NULL, "Last name must be provided");
    last_name_ = new string(last_name);
  }

  ~PersonDescription() {
    delete first_name_;
    delete last_name_;
  }

private:
  PersonDescription(const PersonDescription&);
  PersonDescription& operator=(const PersonDescription&);

  string* first_name_;
  string* last_name_;
};

As we discussed in Part I, our error might not terminate an application, but it might throw an exception. Now we are in trouble: throwing an exception from a constructor could be a bad idea. Let’s consider why this is the case. If you are trying to create an object on the stack and the constructor does its job normally (without throwing an exception), then when the object goes out of scope, the destructor will be called. However, if the constructor did not finish its job because the code of the constructor threw an exception, the destructor will not be called.

Therefore, in the preceding example, if we suppose that the first name was supplied but the second was not, the string for the first name will be allocated but never deleted, and thus we will have a memory leak. However, all is not lost. Let’s look a little deeper into this situation. If we have an object that contains other objects, an important question is: exactly which destructors will be called and which will not?

To answer this question, let’s conduct a small experiment. Suppose we have the following three classes:

class A {
 public:
  A() { cout << "Creating A" << endl; }
  ~A() { cout << "Destroying A" << endl; }
};

class B {
 public:
  B() { cout << "Creating B" << endl; }
  ~B() { cout << "Destroying B" << endl; }
};

class C : public A {
 public:
  C() {
    cout << "Creating C" << endl;
    throw "Don't like C";
  }
  ~C() { cout << "Destroying C" << endl; }

 private:
  B b_;
};

Note that class C contains class B by composition (i.e., we have a data member in C of type B). It also contains the object of type A by inheritance: i.e., somewhere inside the object C there is an object A. Now, what happens if the constructor of C throws an exception? The following code example:

int main() {
   cout << "Testing throwing from constructor." << endl;
  try {
    C c;
  } catch (…) {
    cout << "Caught an exception" << endl;
  }

  return 0;
}

produces this output:

Testing throwing from constructor.
Creating A
Creating B
Creating C
Destroying B
Destroying A
Caught an exception

Note that it is only the destructor of C that was not executed: the destructors of both A and B were called. So the conclusion is simple and logical: for objects whose constructors are allowed to finish normally, the destructors will be called, even if these objects are part of the larger object constructor that did not finish normally. Therefore, let’s rewrite our example with sanity checks using smart pointers:

class PersonDescription {
public:
  PersonDescription(const char* first_name, const char* last_name) {
    SCPP_ASSERT(first_name != NULL, "First name must be provided");
    first_name_ = new string(first_name);

    SCPP_ASSERT(last_name != NULL, "Last name must be provided");
    last_name_ = new string(last_name);
  }

private:
  PersonDescription(const PersonDescription&);
  PersonDescription& operator=(const PersonDescription&);

  scpp::ScopedPtr<string> first_name_;
  scpp::ScopedPtr<string> last_name_;
};

Even if the second sanity check throws an exception, the destructor of the smart pointer to first_name_ will still be called and will do its cleanup. In addition, as a free benefit, we don’t need to worry about initializing these smart pointers to NULL—that is done automatically. So we see that throwing an exception from a constructor is a potentially dangerous business: the corresponding destructor will not be called, and we might have a problem—unless the destructor is empty.

While the C++ community is divided over whether it is a good idea to throw exceptions from constructors, there is a good argument for allowing the constructor to do so. The constructor does not have a return value, so if some of the inputs are wrong, what should we do? One possibility is to just return from the constructor and have a separate class method such as bool IsValid(). And each time you create an object, you should not forget to call my_object.IsValid() and see the result… and you can see where this is going. Which brings us back to the original choice: if something goes wrong inside the constructor, throw an exception. This means that the corresponding destructor will not be called, but this is acceptable to do if that destructor is empty.

Rule for this chapter: to avoid memory leaks when throwing exceptions from a constructor:

  • Design your class in such a way that the destructor is empty.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.176.88