There are four ways to get memory in C++, and most people only ever use one. Then they write a custom allocator or a memory pool, and the difference between “allocate” and “construct” becomes the entire problem.
The Four Mechanisms
new T(args): the only one that does both jobs. Allocates memory by calling operator new(sizeof(T)) internally, then calls T’s constructor on the returned memory. Returns T*. Throws std::bad_alloc on allocation failure. Freed with delete, which calls the destructor then operator delete. No header required for plain new and delete.
operator new(size): allocates only. Returns void*, raw uninitialized bytes, no constructor runs. Throws std::bad_alloc on failure. This is the function new calls internally, and it is a real function subject to overloading, not a language keyword. Overloading it per class redirects all new expressions for that type to a custom allocator without any change at the call site. Freed with operator delete. Also available in a nothrow form: operator new(size, std::nothrow) returns nullptr instead of throwing.
Placement new new (ptr) T(args): constructs only. Takes memory that already exists and builds an object directly inside it. No allocation happens, no memory is reserved. Returns T* pointing to the same address as ptr. Since nothing was allocated, delete cannot be used to clean up. The destructor must be called explicitly: p->~T(). Requires #include <new>.
malloc(size): the C function. Allocates raw bytes, returns void*, must be cast. Returns nullptr on failure, no exception. No constructor ever runs. Freed with free, never delete. Requires #include <cstdlib>.
// allocates AND constructs
Widget* a = new Widget();
delete a; // destructs AND deallocates
// allocates ONLY
void* raw = operator new(sizeof(Widget));
operator delete(raw); // deallocates ONLY, no destructor
// constructs ONLY, in existing memory
alignas(Widget) char buffer[sizeof(Widget)];
Widget* p = new (buffer) Widget();
p->~Widget(); // must call destructor explicitly
// raw bytes ONLY, no constructor
Widget* m = static_cast<Widget*>(malloc(sizeof(Widget)));
free(m); // raw bytes only, no destructor
| Name | Syntax | Allocates | Constructs | Returns | On Failure | Cleanup | Header |
|---|---|---|---|---|---|---|---|
| Regular new | new T(args) |
✓ | ✓ | T* |
throws bad_alloc |
delete |
none |
| Operator new | operator new(size) |
✓ | ✗ | void* |
throws bad_alloc |
operator delete |
none |
| Placement new | new (ptr) T(args) |
✗ | ✓ | T* |
constructor throws | p->~T() |
<new> |
| malloc | malloc(size) |
✓ | ✗ | void* |
returns nullptr |
free |
<cstdlib> |
operator new and malloc both allocate raw bytes with no constructor. new calls operator new internally for its allocation step, then runs the constructor via placement new on the returned memory.
Memory Management
Valid Create / Cleanup Pairs
Every valid combination — and what is UB if you mix incorrectly.
The matching rule
The deallocation function must match the allocation function: new → delete, operator new → operator delete, malloc → free. Objects constructed with placement new must have their destructor called explicitly before the memory is released. External buffers require no deallocation step at all.
Header Requirements
The header requirements are subtle and worth being precise about:
new and delete (plain, standalone): no header required. They are language keywords with built-in compiler support.
operator new and operator delete (the function forms): no header required for the global versions. #include <new> is needed for the nothrow overloads (std::nothrow) and for placement new syntax.
Placement new new (ptr) T(args): requires #include <new>. The same header covers placement delete if needed.
malloc, free, calloc, realloc: #include <cstdlib>.
The <memory> header covers std::construct_at, std::destroy_at, std::destroy, std::destroy_n, and std::allocator, covered later in this post.
The Delete Side: Three Separate Mechanisms
The same split exists on the release side.
delete p: destructs the object at p by calling p->~T(), then calls operator delete(p) to release the memory. Two operations in one keyword. Calling delete on a nullptr is defined and does nothing. Calling it on a pointer that was not obtained from new (a stack pointer, a malloc result, a placement-new pointer) is undefined behavior. Double delete is also undefined behavior.
operator delete(p): releases memory only. No destructor called. The counterpart to operator delete obtained from operator new. Like operator new, it can be overloaded per class.
Placement delete: exists as a concept but is almost never called directly. When a placement new expression (new (ptr) T(args...)) throws during the constructor, the compiler automatically looks for a matching operator delete(void*, void*) to undo any side effects the allocation step may have had. For plain placement new on a raw buffer this is a no-op: no memory was allocated, so there is nothing to release. The compiler calls placement delete as cleanup in error paths, not as a normal cleanup mechanism. Calling it directly in user code is almost always a mistake.
std::construct_at and Friends
C++20 added a set of constexpr-capable wrappers in <memory> that cover the same operations as placement new and explicit destructor calls, with cleaner syntax and constexpr support.
std::construct_at(ptr, args...): the safe constexpr version of placement new. Constructs an object at the already-allocated memory pointed to by ptr. Returns T*. Works in constexpr contexts where placement new syntax cannot be used directly.
#include <memory>
alignas(Widget) char buffer[sizeof(Widget)];
Widget* p = std::construct_at(reinterpret_cast<Widget*>(buffer), args...);
std::destroy_at(ptr): calls ptr->~T() explicitly. The safe wrapper around a manual destructor call. Does not free memory. Constexpr-capable.
std::destroy_at(p); // destructs, does not free
std::destroy(first, last): destructs a range of objects. Calls std::destroy_at on each element from first up to but not including last. Both arguments are pointers or iterators.
std::destroy(widgets, widgets + count);
std::destroy_n(first, n): destructs n objects starting at first. Equivalent to std::destroy(first, first + n). Useful when you have a count rather than an end iterator.
std::destroy_n(widgets, count);
construct_at and destroy_at are the building blocks. destroy and destroy_n are convenience wrappers for ranges. All four are in <memory> and all are constexpr since C++20, making them usable inside constexpr functions where the C++20 transient allocation rules apply.
std::allocator: The STL’s Four-Operation Split
std::allocator<T> is the default allocator used by every STL container. Its design separates memory management from object lifetime management, which is why std::vector can reserve capacity without constructing elements in the reserved slots.
The four operations map directly to the primitives above:
allocate(n): calls operator new(n * sizeof(T)). Reserves raw memory for n objects. No constructor is called. Returns T* pointing to the reserved block.
construct(p, args...): calls placement new on p. Constructs an object in place at the already-allocated location. This is how std::vector::push_back builds the new element in the buffer’s next slot without allocating new memory.
destroy(p): calls p->~T(). Destructs the object without releasing memory. This is how std::vector::pop_back removes an element without shrinking the buffer.
deallocate(p, n): calls operator delete(p). Releases the raw memory. No destructor is called.
The usage order in a container is: allocate first (reserve), then construct as objects are added, then destroy as objects are removed, then deallocate when the container releases its buffer. Memory and object lifetime are managed independently at every step.
In C++17 and later, construct and destroy moved to std::allocator_traits rather than being called directly on the allocator itself, but the underlying operations are identical.
The Pattern They All Enable
Once allocation and construction are separated, one pattern falls out naturally: allocate a large block once up front, then use placement new to construct objects into slots within that block, destroy them with explicit destructor calls when done, and never touch the OS allocator again in the hot path.
// allocate once
void* pool = operator new(N * sizeof(Widget));
Widget* slots = static_cast<Widget*>(pool);
// construct into slots as needed
Widget* w = std::construct_at(slots + i, args...);
// destroy when done with the slot
std::destroy_at(w);
// release the whole block at once
operator delete(pool);
This is the foundation of memory pools, arena allocators, and any lock-free data structure that needs to avoid malloc in its hot path. As covered in the prefaulting post, calling into the allocator at runtime can trigger a page fault on first touch. Pre-allocating the pool and prefaulting it before entering the hot path eliminates both the allocator overhead and the fault latency.
Run: main.cpp
g++ -O2 -std=c++26 main.cpp -o main
./main
Expect Widget constructed and Widget destructed to appear for the new/delete section and the placement new section, and to be absent from the operator new/operator delete and malloc/free sections.
Run: overload.cpp
g++ -O2 -std=c++26 overload.cpp -o overload
./overload
Expect the custom operator new and operator delete messages to appear even though main uses plain new PooledWidget() and delete w syntax.
Output
$ ./main
=== new / delete: allocates and constructs, destructs and deallocates ===
Widget constructed
Widget destructed
=== operator new / operator delete: allocation only, no constructor or destructor ===
(no constructor ran)
(no destructor ran)
=== placement new: construction only, no allocation ===
Widget constructed
(no allocation happened, buffer already existed)
Widget destructed
=== malloc / free: raw bytes only, never touches a constructor or destructor ===
(no constructor ran)
(no destructor ran)
$ ./overload
=== plain 'new PooledWidget()', calling code unchanged ===
custom operator new called, size=1
PooledWidget constructed
PooledWidget destructed
custom operator delete called
Widget constructed and Widget destructed appear only where a constructor and destructor were actually invoked, absent from the operator new and malloc sections. The overload output confirms redirection: the call site uses plain new PooledWidget() and delete w unchanged, but both the custom operator new and operator delete intercept the call. size=1 because PooledWidget has no data members — an empty class still requires a minimum allocation size.
Quick Reference
Coming from other languages
Most languages with automatic memory management hide the allocation/construction split entirely. The runtime handles both as one step and the programmer never sees either separately. C++ exposes the split because it has value semantics, deterministic destruction, and the ability to place objects in arbitrary memory, all of which require being able to construct an object somewhere other than the standard heap. The placement new pattern is how every C++ memory pool is implemented, and understanding it is necessary for writing any container or allocator from scratch.
The 90% mental model
new = allocate + construct. delete = destruct + deallocate. operator new = allocate only, returns void*, can be overloaded per class, no header needed for the global form. Placement new = construct only, into existing memory, requires #include <new>, must call destructor manually, never use delete on it. malloc = raw bytes only, no constructor ever, returns nullptr on failure, freed with free. std::construct_at and std::destroy_at in <memory> are the constexpr-capable wrappers for placement new and explicit destructor calls. std::allocator uses all four primitives internally: allocate and deallocate handle memory, construct and destroy handle object lifetime, kept separate so containers can reserve capacity without constructing objects.