OSC++.020: Virtual Destructors and Polymorphic Cleanup

Technical illustration of a C++ base and derived class hierarchy releasing resources in correct destruction order.

A virtual destructor preserves correct destruction across a polymorphic class hierarchy when an object is deleted through a base-class pointer.

This lesson continues the object-lifetime thread introduced in OSC++.019: Pure Virtual Functions and Abstract Classes Basics, builds on runtime dispatch from OSC++.018: Virtual Functions and Polymorphism Basics, and connects back to cleanup fundamentals from OSC++.016: Constructors and Destructors Basics.

Why destructor dispatch matters

Polymorphism allows a pointer of a base type to refer to an object of a derived type. Destruction creates a separate requirement: if the program destroys that derived object through the base pointer, the base class must support polymorphic destruction.

class Device {
public:
    virtual void report() const = 0;
    virtual ~Device() = default;
};

The virtual destructor makes the destruction operation participate in dynamic dispatch. When a derived object is destroyed through Device*, the derived destructor runs before the base destructor.

Derived object destruction
        ↓
Derived destructor
        ↓
Derived members
        ↓
Base destructor
        ↓
Base members

The unsafe pattern

Consider a base class with a non-virtual destructor:

#include <iostream>

class Device {
public:
    ~Device() {
        std::cout << "Device cleanup\n";
    }
};

class Miner : public Device {
public:
    ~Miner() {
        std::cout << "Miner cleanup\n";
    }
};

int main() {
    Device* device = new Miner;
    delete device;
}

Deleting a derived object through a base pointer whose destructor is non-virtual is undefined behavior in the ordinary polymorphic case. Correctness cannot be inferred from a particular compiler run or from output that merely appears plausible.

cppreference documents the virtual-destructor rule: deleting a derived object through a base pointer requires a virtual base destructor for well-defined polymorphic destruction.

The safe pattern

#include <iostream>

class Device {
public:
    virtual ~Device() {
        std::cout << "Device cleanup\n";
    }
};

class Miner : public Device {
public:
    ~Miner() override {
        std::cout << "Miner cleanup\n";
    }
};

int main() {
    Device* device = new Miner;
    delete device;
}

The intended destruction sequence is:

Miner cleanup
Device cleanup

The derived portion is destroyed first, followed by the base portion. This ordering matches the normal reverse-order destruction of a complete object.

Video 1: Virtual destructors in C++

The Cherno — Virtual Destructors in C++. Demonstrates why polymorphic base classes require correct destructor dispatch.

What virtual actually changes

The static type of the pointer remains the base type:

Device* device = new Miner;

The dynamic type of the allocated object is Miner. A virtual destructor allows the deletion operation to reach the destructor associated with that dynamic type before completing base-class cleanup.

ConceptMeaning
Static typeThe type written in the pointer declaration, such as Device*.
Dynamic typeThe actual runtime object type, such as Miner.
Virtual destructorEnables destruction to follow the runtime object type when deletion occurs through the base interface.

Resource ownership makes the consequence visible

The problem becomes easier to see when the derived class owns resources.

#include <iostream>
#include <memory>

class Device {
public:
    virtual ~Device() = default;
};

class CoolingController : public Device {
    std::unique_ptr<int[]> samples;

public:
    CoolingController()
        : samples(std::make_unique<int[]>(1024)) {}

    ~CoolingController() override {
        std::cout << "CoolingController cleanup\n";
    }
};

The std::unique_ptr member releases its owned array automatically when the derived object is destroyed. Correct polymorphic destruction ensures that the derived destructor and derived members participate in cleanup when ownership flows through the base type.

Public virtual versus protected non-virtual

The C++ Core Guidelines state a widely used design rule: a base-class destructor should generally be either public and virtual, or protected and non-virtual.

C++ Core Guidelines C.35 formalizes this distinction.

// Deletion through Base* is part of
the interface.
class Base {
public:
    virtual ~Base() = default;
};

// Deletion through Base* is intentionally
disallowed.
class Base {
protected:
    ~Base() = default;
};

A protected non-virtual destructor prevents ordinary external code from deleting an object through the base pointer. This can be appropriate when the base type provides an interface but is not intended to own or destroy derived objects polymorphically.

Video 2: V-tables, inheritance, and virtual destructors

DeepDiveDev — C++ Inheritance Explained: V-Tables, Virtual Destructors & Edge Cases. Connects virtual dispatch machinery to safe polymorphic destruction.

Defaulted virtual destructors

A virtual destructor does not require custom cleanup code. A defaulted destructor is often sufficient:

class Device {
public:
    virtual ~Device() = default;
};

This declaration communicates two design facts at once: the type is intended to participate safely in polymorphic destruction, and no custom destructor body is required for the base itself.

Derived destructors inherit virtual behavior

Once the base destructor is virtual, derived destructors are virtual as well. The override specifier remains useful because it documents intent and lets the compiler verify the relationship.

class Device {
public:
    virtual ~Device() = default;
};

class Miner : public Device {
public:
    ~Miner() override = default;
};

Pure virtual destructors

A destructor may also be pure virtual when a base class should remain abstract:

class Device {
public:
    virtual ~Device() = 0;
};

Device::~Device() = default;

The out-of-class definition is still required. Every complete derived-object destruction eventually reaches the base destructor, even when that destructor is pure virtual.

cppreference covers both virtual and pure virtual destructor semantics, including destruction order and the requirement for a pure virtual destructor definition.

Smart pointers do not remove the rule

Modern C++ commonly replaces explicit new and delete with smart pointers. The destructor-design rule still matters when the smart pointer owns a derived object through a base type.

#include <memory>

class Device {
public:
    virtual ~Device() = default;
};

class Miner : public Device {};

int main() {
    std::unique_ptr<Device> device = std::make_unique<Miner>();
}

When device leaves scope, std::unique_ptr<Device> destroys the owned object through the base type. The virtual destructor preserves correct cleanup of the derived object.

Data-center control example

#include <iostream>
#include <memory>
#include <vector>

class Equipment {
public:
    virtual void poll() const = 0;
    virtual ~Equipment() = default;
};

class Pdu : public Equipment {
public:
    void poll() const override {
        std::cout << "PDU telemetry\n";
    }

    ~Pdu() override {
        std::cout << "PDU cleanup\n";
    }
};

class CoolingUnit : public Equipment {
public:
    void poll() const override {
        std::cout << "Cooling telemetry\n";
    }

    ~CoolingUnit() override {
        std::cout << "Cooling cleanup\n";
    }
};

int main() {
    std::vector<std::unique_ptr<Equipment>> equipment;
    equipment.push_back(std::make_unique<Pdu>());
    equipment.push_back(std::make_unique<CoolingUnit>());

    for (const auto& item : equipment) {
        item->poll();
    }
}

The collection owns heterogeneous objects through one base interface. The virtual destructor allows each concrete equipment type to complete its own destruction correctly when the collection is released.

ASIC-management example

class MinerDriver {
public:
    virtual void read_hashrate() = 0;
    virtual ~MinerDriver() = default;
};

class S21Driver : public MinerDriver {
public:
    void read_hashrate() override {
        // model-specific telemetry path
    }

    ~S21Driver() override {
        // release model-specific resources if required
    }
};

A common driver interface can support multiple miner models while preserving model-specific cleanup.

Video 3: Destructor fundamentals with virtual-destructor context

Sudhakar Atchala — Destructors In C++ Programming. Reinforces destructor lifecycle concepts and includes virtual-destructor context.

Common mistakes

  • Deleting a derived object through a base pointer when the base destructor is non-virtual.
  • Assuming that the presence of another virtual function automatically makes the destructor virtual.
  • Adding manual resource cleanup when RAII members such as std::unique_ptr already express ownership safely.
  • Using a public non-virtual destructor on a base type that callers are expected to destroy polymorphically.
  • Forgetting that a pure virtual destructor still requires a definition.
  • Judging undefined behavior only by whether a small test appears to run without crashing.

Exercises

  1. Create a base class named Sensor with a virtual read() function and a virtual defaulted destructor.
  2. Create two derived classes named TemperatureSensor and PressureSensor.
  3. Add destructors to both derived classes that print distinct cleanup messages.
  4. Store both objects in std::vector<std::unique_ptr<Sensor>>.
  5. Allow the vector to leave scope and record the destruction sequence.
  6. Replace the public virtual destructor with a protected non-virtual destructor and observe which ownership patterns no longer compile.
  7. Create a separate abstract base class with a pure virtual destructor and provide the required out-of-class definition.

Knowledge check

1. When is a virtual base destructor required?
When objects may be destroyed through a base-class pointer or owning base-type interface and the dynamic object may be derived.

2. What is the destruction order for a derived object?
The derived destructor runs first, followed by destruction of derived members, then the base destructor and base members according to normal reverse construction order.

3. Does another virtual function automatically make the destructor virtual?
No. The destructor must itself be declared virtual in the base hierarchy.

4. Is a custom destructor body required to obtain polymorphic destruction?
No. virtual ~Base() = default; is often sufficient.

5. Why must a pure virtual destructor still have a definition?
Because base-class destruction still occurs when a complete derived object is destroyed.

6. Do smart pointers eliminate the need for virtual destructors?
No. Owning a derived object through std::unique_ptr<Base> still depends on correct base-class destruction semantics.

Key takeaway

A polymorphic base class that permits destruction through the base interface should provide a public virtual destructor. Correct destructor dispatch preserves the full derived-to-base cleanup sequence and keeps resource ownership aligned with object lifetime.

Base pointer owns derived object
            ↓
Base destructor is virtual
            ↓
Delete or smart-pointer cleanup
            ↓
Derived destructor runs
            ↓
Derived resources released
            ↓
Base destructor runs
            ↓
Complete object cleanup

Display note: all C++ examples and diagrams in this lesson are plain educational code blocks. They are not simulated IDE or terminal interfaces.

BitcoinVersus.Tech

Advertisement

BitcoinVersus.Tech advertisement.

Editor’s Note:

We volunteer daily to ensure the credibility of the information on this platform is Verifiably True. If you would like to support our research initiatives, please donate here: 3C9o19EH5HSiwEPyCTmEKzxhNCbo2X6TTb

BitcoinVersus.tech is not a financial advisor. This media platform reports on financial subjects purely for informational purposes.

Leave a comment