Skip to content

VirtualDestructors

djewsbury edited this page Feb 2, 2015 · 2 revisions

#Destructors in classes with virtual methods

Prefer to always include a virtual destructor in any class with virtual methods.

Consider making that destructor out-of-line in a source file. Consider explicitly defining an out-of-line destructor for all non-trival non-POD classes (even those without virtual methods, and even if the destructor is just empty).

##Example

class ISomeInterface
{
public:
  virtual OnInput(const InputEvent&) const = 0;
  virtual Render() const = 0;

    // we have virtual methods, so we should also have a destructor.
  virtual ~SomeClass();
};

##Rationale

Consider the following case:

class ImplementedInterface : public ISomeInterface
{
public:
  static std::shared_ptr<ISomeInterface> Create();

  virtual OnInput(const InputEvent&) const;
  virtual Render() const;

  ~ImplementedInterface();
private:
  class Pimpl;
  std::unique_ptr<Pimpl> _pimpl;
};

std::shared_ptr<ISomeInterface> ImplementedInterface::Create()
{
  return std::make_shared<ImplementedInterface>();
}

ImplementedInterface::~ImplementedInterface()
{
  // some required cleanup...
}

void Function()
{
  auto ptr = ImplementedInterface::Create();
  ptr->Render();
  ptr.reset();
}

Here, we create a ImplementedInterface and then assign a std::shared_ptr<ISomeInterface> to it. We will then delete it via that pointer to ISomeInterface.

In this case, because we're deleting a pointer of type ISomeInterface, the destructor for ImplementedInterface() will only be called if ISomeInterface has a virtual destructor. Similarly, that members of ImplementedInterface will only be properly cleaned up if there is a virtual destructor.

This is particularly important to remember when writing an interface class that you expect another programmer to implement. We can't predict what other programmers are going to do, so it's best to stay on the safe side.

Since it's common for cases like this to occur for interface classes and class with virtual methods, we can create a general rule. Any class that has virtual methods should also have a virtual destructor.

###Another reason...

There's another important reason. In the C++ compilation process, we start off by compiling "translation units" (eg compiling .cpp files into .obj files), and then those translation units are linked together to create libraries and executables.

When we define an abstract interface class, often that class will only exist in a header file. If all of the methods are abstract with no implementation, then there doesn't need to be any implementation for that class in any .cpp file.

But there does need to be some compiler generated data associated with that class. At the least, this will be a virtual table (but it might also involve type information for RTTI or other compiler features).

That virtual table needs to allocated within one of the executable segments, just like a class-scope static variable. But when we declare a class-scope static variable, we must explicitly definite it in a single .cpp file, right? If we don't define it, we get a linker error. Or if we define it in multiple files, we may also get linker errors. That definition determines which "translation unit" the data should belong to.

So, if the virtual table is just like a hidden class-scope static member, what translation unit does it belong to? The class is defined in a header file, and that header file could be #included into multiple .cpp files. So how does the compiler know where to put it?

If there are some implemented methods, then the problem becomes a little bit easier. The compiler can use the method implementations as a hint, and put the virtual table in the same translation unit.

So, this is another reason to include a virtual table, with an out-of-line implementation in a .cpp file. That gives a hint to the compiler to place the virtual table in the same translation unit. For interfaces that are included into many .cpp files, this could reduce the linker workload a little bit by avoiding many duplicates of the same data.

###Complex compiler-generated destructors

With old C++, we used to put a lot of code in destructors for cleaning up allocations from the constructor. But now, with RAII and smart pointers, frequently classes only need empty destructors. They still have to do the same work (ie, destroying objects that were created in the constructor), but now that code can be automatically generated by the compiler.

In other words, when we use a std::unique_ptr<Object> instead of a raw pointer (like Object*), we know that the object will be automatically destroyed in the destructor. The code for calling the destructor of Object, and freeing the memory is still there. It's just automatically generated as a result of the language rules and the implementation of std::unique_ptr<>.

There's an interesting consequence to this, however. If we don't explicitly declare a destructor, the compiler will generate the destructor for us. But (like the virtual table case above) it can't know which translation unit to add the destructor to. It must generate the destructor for every cpp file that includes the header. These duplicated versions of the destructor will be combined into one in the linking phase.

Automatic destructors can become very complicated very quickly. A class with many members needs to call destructors for all those members. And those members' destructors may call further destructors, and so on. The compiler can end up generating a fair amount of hidden code.

This can also cause header dependency problems, because if we define as std::unique_ptr<Object> member in a class, we can forward declare Object. We don't need the full class definition just to define a pointer to it. But we do need the full class definition for std::unique_ptr<Object>::~unique_ptr(). This means that a compiler generated destructor (or assignment operator) can sometimes require us to include the full class definition.

Both of these problems can be solved by explicitly defining an empty destructor, and putting that destructor in a single .cpp file. This also applies to classes without virtual methods.

Explicit destructors can also be handy in day-to-day debugging, because it provides a easier to read callstack (and a place to put breakpoints).

So, consider creating explicit out-of-line destructors for non-trival classes. This isn't a very critical rule, but it can help make things just a tiny bit cleaner.