Concepts / Destructors and Object Cleanup

Destructors and Object Cleanup

A virtual destructor is a destructor declared as virtual in a base class, and it is common in abstract classes.

  • Programming

A Delete That Misses Part of the Object

A destructor is the cleanup step that runs when an object is destroyed. Many programmers first meet destructors as something that runs when an object goes out of scope, and it is easy to conclude that a destructor only needs to care about the class being written at that moment. If a class holds no resources of its own, the destructor can seem unimportant. This belief breaks down as soon as objects are handled through a base class rather than by their exact type.

What do you think happens?

A Container pointer refers to a Vector object. The pointer is deleted, and the Container destructor is not marked virtual. What happens to the memory and resources the Vector owns?

  • Vector's destructor runs, so its resources are released

  • Only the Container destructor runs, and the Vector's resources can be left unreleased

  • The program refuses to compile

Reveal answer
Answer:

Only the Container destructor runs, and the Vector's resources can be left unreleased.

Without the virtual keyword, the destruction request stops at the Container destructor. The derived part of the object never gets its chance to clean up what it owns.

Tracing a Delete Through a Base Pointer

Consider an abstract class named Container that defines an interface, and a concrete class Vector that implements that interface. Vector owns memory that it allocates for its elements. In the version below, Container has a destructor that is not marked virtual, and a function destroys a Vector through a Container pointer. The point of this example is to watch which destructors run when that pointer is deleted.

Code walkthrough
class Container {
public:
    ~Container() {}
    virtual int size() const = 0;
};

class Vector : public Container {
public:
    Vector(int n) : data(new int[n]), count(n) {}
    ~Vector() { delete[] data; }
    int size() const override { return count; }
private:
    int* data;
    int count;
};

void discard(Container* c) {
    delete c;
}

int main() {
    discard(new Vector(10));
}
  1. 1
    The Container class defines the interface and has no data of its own.
  2. 2
    The Vector class adds a data member that points to heap memory and releases that memory in its destructor.
  3. 3
    The discard function is written against Container alone, the way code that holds a base pointer usually is.
  4. 4
    When delete c runs, the request is handled using the Container destructor, which is not virtual.
  5. 5
    Because the request does not reach Vector's destructor, the array that Vector allocated is not released.
  • Container is abstract because it declares a pure virtual function size().
  • Container's destructor is not marked virtual.
  • Vector allocates an array in its constructor and releases it in its own destructor.
  • discard receives only a Container pointer, so it knows only the interface.
  • delete c is the step where destruction is requested through the base pointer.
discard functioncalls delete on Container*Container destructornot virtual, runsVector destructornever reacheddelete via base pointernot dispatched
When delete is called on a Container pointer that refers to a Vector, only the Container destructor is reached. The Vector destructor is skipped, so its array is left unreleased.

Read the diagram as a sequence of messages. The caller asks for deletion through the Container pointer. Without the keyword, that request stops at the Container destructor. The Vector part of the object is never given its chance to clean up, so the array it allocated can remain unreleased. The program has no visible error at the point of the delete, which is why this problem is easy to miss.

Why the Base Pointer Cannot Know the Answer

Abstract classes such as Container tend to be manipulated through references or pointers. Code that receives such a pointer usually knows only the interface, not the concrete class behind it. The code that holds the pointer may have been written long before the concrete class existed. When someone destroys a Container through a pointer, that code has no idea what resources the underlying implementation owns.

The problem is therefore one of knowledge. The destruction step must be able to follow the actual type of the object, not the type named in the pointer. Declaring the base destructor virtual makes this possible. The destruction request is dispatched to the destructor of the object the pointer actually refers to, so the derived class can release what it owns.

Adding the virtual Keyword

The only change to the hierarchy is the word virtual placed in front of Container's destructor declaration. Everything else stays the same, including the discard function that deletes through a Container pointer.

Code walkthrough
class Container {
public:
    virtual ~Container() {}
    virtual int size() const = 0;
};

class Vector : public Container {
public:
    Vector(int n) : data(new int[n]), count(n) {}
    ~Vector() { delete[] data; }
    int size() const override { return count; }
private:
    int* data;
    int count;
};

void discard(Container* c) {
    delete c;
}

int main() {
    discard(new Vector(10));
}
  1. 1
    The virtual keyword marks the Container destructor as one that is dispatched based on the actual object type.
  2. 2
    When delete c runs, the request is redirected to Vector's destructor, which releases the array.
  3. 3
    After Vector's destructor finishes, the base part of the object is cleaned up through Container's destructor.
  • virtual ~Container() is the only change from the previous version.
  • discard still deletes through a Container pointer, unchanged.
  • Vector's destructor now runs as part of the delete.
delete onContainer*starting pointVector destructorreleases the arrayContainer destructorcleans up the base partdispatched by virtualthen unwinds to base
With a virtual destructor, the call starts at the Container pointer, is dispatched to the Vector destructor, and then unwinds back through the Container destructor.
Container base partcleaned up by basedestructorVector memberscount and data pointerElement arrayreleased only throughVector destructorpoints to
A Container pointer refers to the whole Vector object. The base part is cleaned up by the Container destructor, while the array the Vector owns must be released by the Vector destructor.

The memory layout view explains why the derived destructor matters. The Container pointer covers the base part of the Vector object, but the array that Vector allocated sits outside that base part. Only the Vector destructor knows to release it, which is why the virtual keyword is required for the cleanup to reach that code.

Deciding Which Base Classes Need It

The source ties the need for a virtual destructor to how a base class is used. A base class that acts as an abstract interface is the typical case, because such classes tend to be manipulated through references or pointers. The source notes that this is why a virtual destructor is common in abstract classes. An abstract class like Container does not have a constructor, because it has no data of its own to initialize, but its destructor is still declared virtual.

  • The class is used as a base for other classes that own resources, so a derived object may be destroyed through a pointer to the base.
  • The class is an abstract interface, so code holding it usually cannot know the concrete type behind the pointer.
  • Objects of this hierarchy are manipulated through references or pointers rather than always by their exact type.
  • The destruction of a derived object must reach the derived class's cleanup code, which only the virtual keyword guarantees in this setting.

Mistakes Beginners Make with Base Destructors

  • Common mistake

    Assuming a destructor only needs to clean up the class currently being written

    What it looks like

    Writing Container with an empty non-virtual destructor and assuming Vector's array will still be released through a Container pointer.

    Why it fails

    The destruction request stops at the Container destructor when it is not virtual, so Vector's destructor never runs.

    Do this instead

    Mark the base destructor virtual in any base class that is used through pointers or references.

  • Common mistake

    Believing an abstract class with no data needs no destructor

    Why it fails

    An abstract class like Container can have no data of its own and still be the type through which derived objects are destroyed.

    Do this instead

    Declare the destructor virtual even when the abstract base has no constructor and no members.

  • Common mistake

    Adding virtual only to the derived class destructor and leaving the base destructor non-virtual, which does not change how a base pointer reaches the derived destructor

Checking Your Understanding of the Delete Path

MEDIUM

Suppose a class Shape is an abstract interface with a non-virtual destructor, and a class Circle derives from Shape and allocates a buffer in its own destructor. A function deletes a Shape pointer that refers to a Circle. Trace which destructors run, and state whether the buffer is released.

Hints
  • Ask which destructor the delete request is dispatched to when the base destructor is not virtual.
  • Look at whether the Circle destructor is reached before the Shape destructor finishes.
  • Compare the result with the same code after adding virtual to the Shape destructor.

Key Takeaways

  • A base class used as an abstract interface is often manipulated through references or pointers, and code holding such a pointer knows only the interface, not the resources the concrete class owns.
  • Without a virtual destructor, deleting a derived object through a base pointer can leave the derived part's resources unreleased, because the destruction request stops at the base destructor.
  • Declaring the base destructor virtual lets the destruction request follow the actual object's type, reaching the derived class's cleanup code before the base part is cleaned up.
  • A base class meant to be extended through pointers or references should have a virtual destructor, even when it is abstract and has no data or constructor of its own.

Related concepts

More concepts starting with D