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:
- 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. - Global Scope: Replaces the default
::operator deleteacross 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 newmandates overridingoperator 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 deleteimplementation must safely accept and ignore anullptrargument without raising an exception.