CodeNebula
C++20coreadvanced

Destroying Delete (C++20 Addition)

C++20 · The Big Four · Compiler flag: -std=c++20

Explanation

A new form of `operator delete` that receives the object before its destructor runs, giving the delete operator full control over destruction. Declared by adding a `std::destroying_delete_t` tag parameter. This enables advanced memory management patterns such as classes that store their size alongside the object.

Real-World Scenario

Implement a variable-length object (like a string with inline storage) where the deallocation size depends on runtime data stored in the object itself, which would normally be destroyed before `operator delete` sees it.

Quick Snippet

snippet.cpp
#include <new>
#include <cstddef>

struct VariableBuffer {
    std::size_t size;
    char data[];  // flexible array member (compiler extension)

    void operator delete(VariableBuffer* ptr, std::destroying_delete_t) {
        std::size_t total = sizeof(VariableBuffer) + ptr->size;
        ptr->~VariableBuffer();
        ::operator delete(ptr, total);
    }

    static VariableBuffer* create(std::size_t n) {
        void* mem = ::operator new(sizeof(VariableBuffer) + n);
        auto* buf = new (mem) VariableBuffer{n, {}};
        return buf;
    }
};

Full Example

destroying_delete.cpp
// CodeNebula - C++20 Example: std::destroying_delete_t
// Compile: g++ -std=c++20 -Wall -Wextra -o example destroying_delete.cpp

// std::destroying_delete_t lets operator delete receive the object
// BEFORE the destructor runs. This enables:
//   1. Custom deallocation based on runtime object state
//   2. Variable-size allocations where the object knows its true size
//   3. Classes that control their own destruction sequence

#include <iostream>
#include <new>
#include <cstdlib>
#include <cstring>

// --- Example 1: Object that tracks its own allocation size ---
struct FlexibleBuffer {
    std::size_t alloc_size;
    char data[1];  // flexible array member pattern

    // Factory: allocate extra bytes for the data
    static FlexibleBuffer* create(std::size_t data_size) {
        std::size_t total = sizeof(FlexibleBuffer) + data_size - 1;
        void* mem = ::operator new(total);
        auto* obj = new (mem) FlexibleBuffer();
        obj->alloc_size = total;
        std::memset(obj->data, 0, data_size);
        std::cout << "  Allocated " << total << " bytes\n";
        return obj;
    }

    // Destroying delete: we get the object before destruction
    void operator delete(FlexibleBuffer* ptr, std::destroying_delete_t) {
        std::size_t size = ptr->alloc_size;  // read BEFORE destroying
        ptr->~FlexibleBuffer();               // manually call destructor
        std::cout << "  Deallocating " << size << " bytes\n";
        ::operator delete(ptr);
    }
};

// --- Example 2: Prevent deletion of derived types through base pointer ---
struct SafeBase {
    int id;
    SafeBase(int i) : id(i) { std::cout << "  SafeBase(" << id << ") constructed\n"; }
    ~SafeBase() { std::cout << "  SafeBase(" << id << ") destroyed\n"; }

    // This destroying delete controls the deletion process
    void operator delete(SafeBase* ptr, std::destroying_delete_t) {
        std::cout << "  Destroying delete called for SafeBase id=" << ptr->id << "\n";
        ptr->~SafeBase();
        ::operator delete(ptr);
    }
};

// --- Example 3: Conditional destruction ---
struct MaybePooled {
    bool from_pool;
    int value;

    MaybePooled(bool pooled, int v) : from_pool(pooled), value(v) {
        std::cout << "  Created MaybePooled(" << value << ") "
                  << (from_pool ? "[pooled]" : "[heap]") << "\n";
    }

    ~MaybePooled() {
        std::cout << "  Destroyed MaybePooled(" << value << ")\n";
    }

    void operator delete(MaybePooled* ptr, std::destroying_delete_t) {
        bool pooled = ptr->from_pool;
        int val = ptr->value;
        ptr->~MaybePooled();  // manually destroy

        if (pooled) {
            std::cout << "  Returning " << val << " to pool (simulated)\n";
            ::operator delete(ptr);  // in real code, return to pool
        } else {
            std::cout << "  Freeing " << val << " from heap\n";
            ::operator delete(ptr);
        }
    }
};

int main() {
    std::cout << "=== std::destroying_delete_t ===\n\n";

    // Example 1: Variable-size deallocation
    std::cout << "FlexibleBuffer (100 bytes of data):\n";
    FlexibleBuffer* buf = FlexibleBuffer::create(100);
    delete buf;
    std::cout << "\n";

    // Example 2: SafeBase deletion
    std::cout << "SafeBase:\n";
    SafeBase* sb = new SafeBase(42);
    delete sb;
    std::cout << "\n";

    // Example 3: Conditional deallocation strategy
    std::cout << "Conditional deallocation:\n";
    MaybePooled* heap_obj = new MaybePooled(false, 10);
    MaybePooled* pool_obj = new MaybePooled(true, 20);
    delete heap_obj;
    delete pool_obj;

    std::cout << "\nKey point: destroying_delete receives the live object,\n"
              << "so it can inspect state before calling the destructor.\n";

    return 0;
}

// Expected output:
// === std::destroying_delete_t ===
//
// FlexibleBuffer (100 bytes of data):
//   Allocated ... bytes
//   Deallocating ... bytes
//
// SafeBase:
//   SafeBase(42) constructed
//   Destroying delete called for SafeBase id=42
//   SafeBase(42) destroyed
//
// Conditional deallocation:
//   Created MaybePooled(10) [heap]
//   Created MaybePooled(20) [pooled]
//   Destroyed MaybePooled(10)
//   Freeing 10 from heap
//   Destroyed MaybePooled(20)
//   Returning 20 to pool (simulated)
//
// Key point: destroying_delete receives the live object,
// so it can inspect state before calling the destructor.
Run command
g++ -std=c++20 -Wall -Wextra -o example destroying_delete.cpp