Skip to content
C++
Language
Intermediate

New and Delete Operators

How new/delete expressions, operator new/delete allocation functions, placement and nothrow forms, class-specific overloads, and the new-handler work in C++.

new / deletesince C++98

A new-expression calls an allocation function (operator new) to obtain raw storage and then constructs an object in it, while a delete-expression runs the destructor and then calls a deallocation function (operator delete) to release the storage.

Overview

The vocabulary matters, because two distinct things share the name new:

  • new-expression / delete-expression: the language constructs (new T(args), delete p). They cannot be overloaded. They sequence allocation, construction, destruction and deallocation.
  • operator new / operator delete: ordinary functions (the allocation and deallocation functions) that you can replace or overload. They deal only in raw bytes.

new T(args) is roughly: call operator new(sizeof(T)); construct T in the result; if the constructor throws, call the matching operator delete and rethrow. delete p is: if p is non-null, call p->~T(), then operator delete(p). Deleting a null pointer is a no-op.

Array forms (new T[n], delete[] p) use operator new[] / operator delete[], construct n elements in order and destroy them in reverse order. The array form must be paired with delete[]; mixing forms is undefined behavior.

In modern C++ (since C++11) prefer std::make_unique / std::make_shared (C++14 for make_unique) and containers over raw new. You still need to understand these mechanics for custom allocators, pools, and legacy code.

Syntax

The standard <new> header declares these replaceable global functions:

cpp
#include <new>

void* operator new(std::size_t);                                  // C++98, throws std::bad_alloc
void* operator new(std::size_t, const std::nothrow_t&) noexcept;  // C++98, returns nullptr
void* operator new(std::size_t, std::align_val_t);                // C++17, over-aligned
void* operator new(std::size_t, void* ptr) noexcept;              // C++98, placement (not replaceable)

void  operator delete(void*) noexcept;                            // C++98
void  operator delete(void*, std::size_t) noexcept;               // C++14, sized deallocation
void  operator delete(void*, std::align_val_t) noexcept;          // C++17
void  operator delete(void*, std::size_t, std::align_val_t) noexcept; // C++17
// operator new[] / operator delete[] mirror all of the above

Notes on versions:

  • Sized operator delete was added in C++14; compilers may need -fsized-deallocation (GCC/Clang enable it by default from C++14 onward on most targets).
  • Over-aligned allocation (types with alignas greater than __STDCPP_DEFAULT_NEW_ALIGNMENT__) is automatic since C++17. Before that, new of an over-aligned type was not guaranteed to honor alignment.
  • Since C++20, a class may declare a destroying operator delete(T*, std::destroying_delete_t), which takes over destructor invocation as well.
  • Placement operator new and its matching delete cannot be replaced.

Examples

Allocation failure and nothrow

cpp
#include <new>
#include <iostream>

int main() {
    try {
        auto* big = new char[~std::size_t{0} / 2];   // throws std::bad_alloc
        delete[] big;
    } catch (const std::bad_alloc& e) {
        std::cerr << "allocation failed: " << e.what() << '\n';
    }

    int* p = new (std::nothrow) int[1'000'000];      // nullptr on failure
    if (!p) return 1;
    delete[] p;
}

Placement new into an arena

Placement new constructs an object at a caller-supplied address. You are then responsible for calling the destructor explicitly; no deallocation happens.

cpp
#include <new>
#include <cstddef>
#include <string>
#include <utility>

class Arena {
    alignas(std::max_align_t) unsigned char buf_[4096];
    std::size_t used_ = 0;
public:
    template <class T, class... Args>
    T* create(Args&&... args) {
        std::size_t start = (used_ + alignof(T) - 1) & ~(alignof(T) - 1);
        if (start + sizeof(T) > sizeof buf_) throw std::bad_alloc{};
        used_ = start + sizeof(T);
        return ::new (static_cast<void*>(buf_ + start)) T(std::forward<Args>(args)...);
    }
};

int main() {
    Arena a;
    std::string* s = a.create<std::string>("hello, arena");
    s->~basic_string();            // explicit destructor; storage is released with the arena
    // C++17: std::destroy_at(s) is the clearer spelling
}

Use ::new so a class-specific operator new cannot hijack the call. Since C++20, std::construct_at is the constexpr-friendly alternative.

Class-specific overloads with a free list

cpp
#include <new>
#include <cstddef>
#include <cstdlib>

class Node {
    struct FreeSlot { FreeSlot* next; };
    static inline FreeSlot* free_ = nullptr;   // C++17 inline variable
    int value_[4]{};
public:
    static void* operator new(std::size_t sz) {
        if (sz != sizeof(Node)) return ::operator new(sz);   // derived class: fall back
        if (free_) { void* p = free_; free_ = free_->next; return p; }
        return ::operator new(sizeof(Node));
    }
    static void operator delete(void* p, std::size_t sz) noexcept {  // sized: C++14
        if (!p) return;
        if (sz != sizeof(Node)) { ::operator delete(p); return; }
        auto* s = static_cast<FreeSlot*>(p);
        s->next = free_;
        free_ = s;
    }
};

The size check matters: a derived class inherits these functions, and sz will be sizeof(Derived). Give the base a virtual destructor so delete basePtr selects the right size.

Customizing out-of-memory behavior

cpp
#include <new>
#include <cstdlib>
#include <cstdio>

void on_oom() {
    std::fputs("out of memory, aborting\n", stderr);
    std::abort();
}

int main() {
    std::set_new_handler(on_oom);   // C++98; get_new_handler is C++11
    // ...
}

A global replacement operator new loops: attempt allocation, and on failure call the current new-handler and retry. The handler must free memory, install a different handler, throw bad_alloc (or derived), or terminate.

cpp
void* operator new(std::size_t n) {          // replaces global version
    for (;;) {
        if (void* p = std::malloc(n ? n : 1)) return p;
        if (auto h = std::get_new_handler()) h();   // C++11
        else throw std::bad_alloc{};
    }
}
void operator delete(void* p) noexcept { std::free(p); }
void operator delete(void* p, std::size_t) noexcept { std::free(p); }  // C++14

Best Practices

  • Prefer RAII. Use std::unique_ptr, std::make_shared, std::vector, or std::pmr containers (C++17) instead of naked new.
  • Replace in pairs. Whenever you replace or overload operator new, supply the matching operator delete (including sized and aligned variants you might hit). A mismatch between your allocator and free/default delete is undefined behavior.
  • Prefer class-specific overloads over replacing the global ones, unless you are intentionally instrumenting the whole program.
  • Provide all three forms (plain, nothrow, placement) in a class that declares any operator new, or add using ::operator new;, because a class-scope declaration hides the global overloads.
  • Pair placement-new with a matching placement-delete. If a constructor throws in new (args) T, the runtime calls operator delete(void*, args...). If none exists, nothing is called and the storage leaks.
  • Keep deallocation functions noexcept and never let exceptions escape them or destructors.
  • Use ::new in generic code that constructs into raw storage.

Common Pitfalls

  • delete vs delete[] mismatch. new T[n] requires delete[]. Using delete is undefined behavior, and may destroy only one element or corrupt the heap. Array allocation may store a hidden element count, so the pointer from operator new[] can differ from the one returned to you.
  • Treating operator new as a constructor. It returns raw bytes; no object exists until construction. Casting the result and reading from it is UB.
  • Forgetting the new-handler isn't called by custom allocators. If you write operator new with malloc, you must invoke the handler yourself (see above) and throw bad_alloc on failure; returning nullptr from a throwing operator new is not permitted.
  • Double delete / use after delete. Set owning raw pointers to nullptr only as a stopgap; the real fix is smart pointers.
  • Leaks from partially-constructed arrays in hand-rolled containers. When writing new T[n] replacements with placement new, destroy already-built elements if a later constructor throws.
  • Mixing allocators. Memory from malloc must go to free, memory from new to delete. Never cross them.
  • Over-aligned types before C++17. new did not reliably honor extended alignment; use std::aligned_alloc or a custom allocator, or move to C++17.
  • Unsized delete on polymorphic types without a virtual destructor. Deleting a derived object through a base pointer is undefined behavior, and a sized operator delete receives the wrong size.
  • new expression evaluation order. Before C++17, f(std::unique_ptr<T>(new T), g()) could leak if g() threw between allocation and ownership. C++17 tightened sequencing, but make_unique remains the safest idiom.

See Also