Overriding Global New and Custom Delete in C++

In C++, memory management can be fine-tuned beyond the standard runtime heap by utilizing custom delete operators and overriding the global operator new. This article explains the mechanics of custom operator delete, how it interacts with memory deallocation, and the specific scenarios—such as performance optimization, debugging, and memory tracking—where an application benefits from replacing the global dynamic allocation operators.

What Are Custom Delete Operators?

In C++, invoking the delete expression performs two tasks: it calls the object's destructor and then releases the allocated memory by invoking operator delete. A custom delete operator is a user-defined overload of the deallocation function, replacing the default implementation provided by the C++ standard library.

Custom delete operators can be implemented at two levels:

  1. Class-Specific Scope: Defined as a static member function within a class. Whenever an instance of that class is destroyed via delete, the compiler calls the class-specific deallocation function instead of the global one.
  2. Global Scope: Replaces the default ::operator delete across the entire application.
// Class-specific custom delete example
class Buffer {
public:
    static void* operator new(std::size_t size) {
        // Custom allocation logic
        return ::operator new(size);
    }

    static void operator delete(void* ptr) noexcept {
        // Custom deallocation logic
        ::operator delete(ptr);
    }

    // Sized deallocation (C++14 and later)
    static void operator delete(void* ptr, std::size_t size) noexcept {
        ::operator delete(ptr);
    }
};

Additionally, C++ allows placement delete operators. A placement delete corresponds directly to an overloaded placement new with identical extra parameters. It is called automatically by the runtime only if an exception is thrown during object construction, ensuring that allocated memory is freed before the exception propagates.

When Would an Application Override Global operator new?

Overriding the global operator new (and its required counterpart, global operator delete) alters dynamic memory allocation for the entire binary. While standard library allocators are general-purpose and resilient, they are not optimized for every workload.

Applications typically override global allocation routines for the following reasons:

1. Performance and Fragmentation Reduction

General-purpose allocators utilize locks to maintain thread safety and use complex algorithms to prevent external fragmentation across varying allocation sizes. In high-performance applications (such as game engines, high-frequency trading platforms, or real-time simulation software), these default mechanisms introduce significant latency. Overriding global new allows teams to route all allocations to high-performance allocators such as jemalloc, TCMalloc, or custom thread-local slab and arena allocators.

2. Memory Profiling, Tracking, and Leak Detection

Overriding global new and delete enables instrumentation at the core allocation layer without modifying third-party dependencies or existing application code. Engineers can track:

  • Total allocated bytes over time.
  • High-water memory usage marks.
  • Allocation call stacks to locate memory leaks.
  • Real-time allocation rates to identify unintended allocations in performance-critical code paths.

3. Safety, Debugging, and Guard Bands

A customized global allocator can insert metadata, such as guard bands (canaries), immediately before and after the allocated block. During delete, the allocator inspects these boundaries to detect buffer overflows or underflows. Overridden allocators can also scrub deallocated memory with a specific bit pattern (e.g., 0xDEADBEEF) to catch use-after-free errors immediately.

4. Hard Constraints and Embedded Systems

In embedded environments or mission-critical real-time systems, dynamic heap allocation may be strictly prohibited after system initialization. By overriding global operator new, developers can:

  • Redirect allocations to fixed-size memory pools.
  • Enforce strict memory limits.
  • Assert or throw errors if code attempts to allocate dynamically during runtime phases where deterministic execution is required.

Key Rules for Overriding

When overriding global allocation and deallocation functions:

  • Always match allocations with deallocations: Overriding operator new mandates overriding operator delete, including array variants (operator new[], operator delete[]) and sized deallocation variants introduced in C++14.
  • Respect alignment requirements: Modern C++ (C++17 onward) requires support for over-aligned types using overloads that accept std::align_val_t.
  • Handle null pointers safely: An operator delete implementation must safely accept and ignore a nullptr argument without raising an exception.