Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 50

Virtual Destructors Explained

Recommended resources

Introduction

Hello! In our last lesson, we dove into how virtual functions enable dynamic polymorphism, allowing us to call derived-class methods through a base-class pointer. We saw that this mechanism, powered by v-tables, is key to writing flexible and generic code.

We ended with a crucial question: what happens during cleanup? Specifically, what happens if we allocate a derived object on the heap and then delete it through a base-class pointer?

Base* ptr = new Derived();
// ... do some work ...
delete ptr; // Is this safe?

This lesson will answer that question. You will learn about a common and dangerous bug in C++ inheritance—the resource leak caused by improper destruction—and how to prevent it. Our focus is on explaining the necessity of a virtual destructor in a base class. This is one of the most important rules to follow when designing class hierarchies.


1. The Problem: A Silent Memory Leak

When you call delete ptr, the compiler needs to know which destructor to call. In the absence of any special instructions, it uses static binding, just as we saw with regular member functions in the previous lesson. It looks at the static type of the pointer (Base*) and decides to call the Base class destructor.

The problem? The Derived class destructor is never called. If the Derived class manages any resources—most commonly, memory allocated in its own constructor—those resources will never be released. This is a classic memory leak.

Let's watch a video that demonstrates this problem practically.

Virtual Destructors in C++

The Cherno provides a concise and clear demonstration of this exact issue. He'll set up a base and derived class, create a polymorphic object, and show how deleting it through a base pointer fails to call the derived destructor.

Watch from 03:20 to 05:58. Pay close attention to the code that creates a Derived instance but assigns it to a Base pointer. Observe the console output when delete is called—only the base destructor runs. The example is then enhanced to show a concrete memory leak by allocating an array inside the derived class.

As you saw, the Derived constructor allocated memory for an array, but because the Derived destructor was never called, the delete[] for that array was never executed. This is a silent but serious bug that can cause your programs to consume more and more memory over time until they eventually crash.


2. The Solution: Virtual Destructors and the V-Table

The solution is to apply the same principle we used for member functions: dynamic dispatch. We need to tell the compiler that the destructor call should be resolved at runtime based on the actual type of the object. We do this by declaring the destructor in the base class as virtual.

class Base {
public:
    // The destructor is now virtual!
    virtual ~Base() { /* ... */ } 
    // ...
};

When the destructor is virtual, it gets an entry in the class's v-table. Now, when delete ptr; is executed:

  1. The runtime system follows the object's v-ptr to its v-table (which is the Derived class's v-table).
  2. It looks up the destructor in the v-table and finds the address of ~Derived().
  3. It calls the Derived destructor (~Derived()).
  4. Crucially, after the Derived destructor finishes, the compiler automatically ensures that the Base class destructor (~Base()) is also called.

This creates a "chain of destruction" in the reverse order of construction, guaranteeing that all resources at every level of the hierarchy are cleaned up correctly.

Let's see the fix in action.

Virtual Destructors in C++

Let's return to The Cherno's video to see how this one-word change fixes the memory leak.

Watch from 05:58 to 06:36. Notice how adding virtual to the base destructor results in both the derived and base destructors being called, in the correct order.

To connect this directly back to our discussion of v-tables from the last lesson, the following video gives a great summary of why this works.

C++ Inheritance Explained: V-Tables, Virtual Destructors & Edge Cases

This clip from DeepDiveDev explains the problem and solution from the perspective of the v-table, reinforcing the 'under the hood' mechanics.

Watch the segment from 14:43 to 16:06. The explanation clearly links the virtual keyword on the destructor to adding it to the v-table, which enables the dynamic dispatch necessary for correct cleanup.


3. A Rule of Thumb and Its Justification

You might be tempted to just make all destructors virtual "just in case." However, this isn't always the right approach. Since you're interested in how systems work, it's important to understand the trade-offs.

Declaring a function virtual is not free. For a class that has no other virtual functions, making the destructor virtual:

  1. Adds a v-ptr to every object of that class, increasing its memory footprint (typically by the size of a pointer, 8 bytes on a 64-bit system).
  2. Creates a v-table for the class, adding to the program's static data size.
  3. Makes the class non-POD (Plain Old Data). This prevents you from performing certain low-level memory operations like using memcpy on objects of this class, which is relevant for high-performance or systems-level code.

So, when should a destructor be virtual? The renowned C++ expert Scott Meyers provided the canonical rule of thumb.

Virtual Destructors in C++. Necessity, Good Practice, Bad Practice

The following article provides a fantastic, nuanced discussion of when virtual destructors are a necessity, when they are good practice, and when they are bad practice. It also contains the key quote from Scott Meyers.

Please read the entire article. It's well-structured and directly addresses our question. Focus on 'Type 1', which describes the exact scenario we've been discussing: it's a necessity. Read 'Type 2'. This covers cases where a class already has virtual functions. The overhead is already paid, so making the destructor virtual is good practice as a safety measure. Finally, read 'Type 3', which explains why making destructors virtual for classes not designed for polymorphic use is bad practice due to the unnecessary overhead. The article concludes with a quote from Scott Meyers' Effective C++, which perfectly summarizes the best practice.

The key takeaway, as summarized by Meyers, is: Polymorphic base classes should declare virtual destructors. If a class has any virtual functions, it is intended to be used polymorphically. Therefore, it's almost certain that a client will try to delete a derived object through a base pointer, and the destructor must be virtual to handle this safely. If a class is not designed to be a base class or is not used polymorphically, it should not have a virtual destructor.


Conclusion

In this lesson, we've tackled a critical aspect of object-oriented programming in C++. You've seen how deleting derived objects through base class pointers can lead to resource leaks if the destructor isn't virtual, and you now understand the mechanism that fixes this problem.

Key Takeaways:

  • Deleting a derived object through a base class pointer results in undefined behavior if the base class destructor is not virtual. Typically, this means the derived destructor is not called, leading to resource leaks.
  • Declaring the base class destructor as virtual places it in the v-table, enabling dynamic dispatch to ensure the correct destructor (~Derived()) is called at runtime.
  • After the derived destructor runs, the base class destructor is automatically called, ensuring a complete and orderly cleanup.
  • The Rule: If your class is designed to be a polymorphic base class (i.e., it has at least one virtual function), you must declare its destructor virtual. If it's not meant for polymorphic use, you should not, to avoid unnecessary overhead.

Next Up

We've established that virtual functions allow derived classes to optionally override base class behavior. But what if we want to define an "interface" and force derived classes to provide an implementation for a specific function?

In our next lesson, we will explore abstract base classes and pure virtual functions. This mechanism allows you to define contracts that subclasses must adhere to, forming the foundation of many powerful design patterns.