Destructors

Definition: A destructor is a special member function that cleans up an object’s resources when it goes out of scope or is explicitly deleted. The destructor is automatically called: you don’t invoke it manually.


Key Characteristics

Naming

~ClassName()

(e.g., ~Vehicle())

Signature

No parameters, no return type

Unlike constructors

One Per Class

Cannot be overloaded

Only one destructor

Automatic

Called at scope exit

or delete


What Does a Destructor Do?

A destructor performs cleanup operations before an object is destroyed:

Memory Management

Release dynamically allocated memory

delete, delete[]

Close Resources

Close file handles, network connections, database connections

Release Locks

Release mutexes or other synchronization primitives

Reference Counting

Decrement reference counts in shared ownership schemes

Example: RAII Pattern

Listing 19 Automatic Resource Management
class FileHandler {
private:
    FILE* file_;
public:
    FileHandler(const char* filename) : file_(fopen(filename, "r")) {}

    ~FileHandler() {  // Destructor ensures file is closed
        if (file_) {
            fclose(file_);
        }
    }
};
// file_ is automatically closed when FileHandler goes out of scope

Destructor Execution Order

⚠️ Critical Concept

Destructors are called in the reverse order of constructor calls

Listing 20 Inheritance Destruction Order
class Base {
public:
    Base() { std::cout << "Base constructor\n"; }
    ~Base() { std::cout << "Base destructor\n"; }
};

class Derived : public Base {
public:
    Derived() { std::cout << "Derived constructor\n"; }
    ~Derived() { std::cout << "Derived destructor\n"; }
};

int main() {
    Derived d;
}

Output:

Base constructor
Derived constructor
Derived destructor    ← Reversed order
Base destructor       ← Reversed order

Why This Matters:

  1. Derived class resources are cleaned up first

  2. Base class resources are cleaned up last

  3. No dangling references during destruction


virtual Destructors and Inheritance

⚠️ CRITICAL WARNING

When deleting through a base pointer, a virtual destructor ensures the derived class destructor runs first. Without it, only the base destructor runs, causing resource leaks and undefined behavior.

The Problem

❌ Non-Virtual Destructor Bug
Listing 21 Memory Leak with Non-Virtual Destructor
class Base {
public:
    ~Base() { std::cout << "~Base()\n"; }  // NOT virtual!
};

class Derived : public Base {
private:
    int* data_;
public:
    Derived() : data_(new int[1000]) {}

    ~Derived() {
        delete[] data_;
        std::cout << "~Derived()\n";
    }
};

Base* ptr = new Derived();
delete ptr;  // UNDEFINED BEHAVIOR!
             // Only ~Base() called, not ~Derived()
             // Leaks memory from data_!

Warning

Without virtual, the destructor is selected at compile time based on the pointer type (Base*), not the actual object type (Derived). The derived destructor never runs!

The Solution

✅ Virtual Destructor Solution
Listing 22 Correct Behavior with Virtual Destructor
class Base {
public:
    virtual ~Base() { std::cout << "~Base()\n"; }  // Virtual!
};

class Derived : public Base {
private:
    int* data_;
public:
    Derived() : data_(new int[1000]) {}

    ~Derived() override {
        delete[] data_;
        std::cout << "~Derived()\n";
    }
};

Base* ptr = new Derived();
delete ptr;  // Correct behavior
             // Calls ~Derived() first, then ~Base()
             // No memory leak!

// Output:
// ~Derived()
// ~Base()

Important

Golden Rule: Any class intended to be a polymorphic base class must have a virtual destructor.

When to Use Virtual Destructors

✅ Always Use Virtual Destructors

When:

  • The class has any virtual methods

  • The class is designed to be inherited from

  • Objects may be deleted through base class pointers

  • The class is part of a polymorphic hierarchy

🚫 Don’t Need Virtual Destructors

When:

  • The class is final (cannot be inherited)

  • The class is never used polymorphically

  • No derived instances deleted through base pointers


The = default Keyword

Definition: The = default keyword requests the compiler generate the default implementation of a special member function (constructor, destructor, copy/move operations).

Why Use It?

Explicit Intent

Shows you deliberately want default behavior

Optimization

Compiler-generated code is often optimal

Rule of Zero

Don’t write what compiler can generate

Special Rules

Enables trivial/constexpr functions

Example:

Listing 23 Using = default
class Vehicle {
public:
    virtual ~Vehicle() = default;         // Compiler generates virtual destructor body
    Vehicle() = default;                  // Compiler generates default constructor
    Vehicle(const Vehicle&) = default;    // Compiler generates copy constructor
};

Note

= default is particularly important for virtual destructors in abstract base classes where you don’t need custom cleanup logic but must ensure the destructor is virtual.

When to Use = default

Use = default for destructors when:

  • You need a virtual destructor but have no cleanup logic

  • You are following the Rule of Zero

  • You want to explicitly document default behavior

class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() = 0;
};

Write a custom destructor when:

  • You need to release manually managed resources

  • You need to close file handles or network connections

  • You need to perform logging or side effects

  • You are managing RAII wrappers

class Connection {
private:
    int socket_fd_;
public:
    ~Connection() {
        close(socket_fd_);  // Custom cleanup
    }
};

Common Patterns

Listing 24 Abstract Base with Virtual Destructor
class Shape {
public:
    virtual ~Shape() = default;           // Virtual destructor
    virtual double area() const = 0;      // Pure virtual
    virtual void draw() const = 0;        // Pure virtual
};

class Circle : public Shape {
private:
    double radius_;
public:
    ~Circle() override = default;         // No cleanup needed
    double area() const override {
        return 3.14159 * radius_ * radius_;
    }
    void draw() const override { /* drawing code */ }
};
Listing 25 RAII Class with Custom Destructor
class DatabaseConnection {
private:
    void* connection_;  // Opaque handle
public:
    DatabaseConnection(const char* host) {
        connection_ = connect_to_database(host);
    }

    ~DatabaseConnection() {
        if (connection_) {
            close_connection(connection_);
            connection_ = nullptr;
        }
    }

    // Delete copy operations (connection is unique)
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;
};
Listing 26 Inheritance with Resource Management
class Vehicle {
public:
    virtual ~Vehicle() = default;  // Virtual for polymorphism
    virtual void drive() = 0;
};

class RoboTaxi : public Vehicle {
private:
    std::unique_ptr<SensorArray> sensors_;  // RAII wrapper
    std::unique_ptr<NavigationSystem> nav_; // RAII wrapper
public:
    // Compiler-generated destructor automatically cleans up unique_ptrs
    ~RoboTaxi() override = default;

    void drive() override { /* ... */ }
};

Common Pitfalls

❌ Pitfall 1: Forgetting Virtual Destructor
class Base {
public:
    virtual void foo() = 0;
    // Missing: virtual ~Base() = default;
};

std::unique_ptr<Base> ptr = std::make_unique<Derived>();
// When ptr goes out of scope: UNDEFINED BEHAVIOR!

Fix: Always add virtual destructor to polymorphic bases.

❌ Pitfall 2: Calling Virtual Functions in Destructors
class Base {
public:
    virtual ~Base() {
        cleanup();  // Does NOT use derived version!
    }
    virtual void cleanup() { /* base cleanup */ }
};

class Derived : public Base {
public:
    ~Derived() override { /* ... */ }
    void cleanup() override { /* derived cleanup */ }
};

Warning

Virtual dispatch does NOT work in constructors or destructors. Only the current class’s version is called.

❌ Pitfall 3: Exception Safety
class Resource {
public:
    ~Resource() {
        // Never throw from destructors!
        // If this throws, std::terminate is called
        cleanup();
    }
};

Important

Destructors should be noexcept (they are by default). Never let exceptions escape from destructors.


Best Practices

1. Virtual in Base Classes

Always make base class destructors virtual

Show Example
class Base {
public:
    virtual ~Base() = default;
};
2. Use = default

Use = default when no custom cleanup needed

Show Example
class Widget {
public:
    virtual ~Widget() = default;
};
3. Rule of Zero

Let compiler generate destructors when possible

  • Use RAII wrappers

  • unique_ptr, shared_ptr

  • Only write custom when managing raw resources

4. Use override

Mark overridden destructors with override

Show Example
class Derived : public Base {
public:
    ~Derived() override = default;
};
5. Never Throw

Never throw from destructors

  • Implicitly noexcept

  • Throwing calls std::terminate

6. Reverse Order

Clean up in reverse order of construction

  • Compiler does automatically

  • Ensures no dangling references


Core Concepts:

  • Destructors clean up resources automatically when objects are destroyed

  • Use virtual destructors in polymorphic base classes

  • Use = default for virtual destructors when no cleanup is needed

  • Never throw exceptions from destructors

  • Follow the Rule of Zero: prefer RAII wrappers over manual management

  • Always use override when overriding virtual destructors

  • Virtual dispatch doesn’t work in constructors/destructors

  • Destructors execute in reverse order of construction

Automatic

Called automatically

Reverse Order

Opposite of construction

Virtual Required

For polymorphic bases

RAII Pattern

Resource management