Ch20.7: Default Heap Storage: new and delete
1. The Feature
new is not one operation. It is three, and only one of them is visible in the
source: ask a global allocation function for memory, run the constructor in it, and decide
what to do if the allocation function fails. delete is the mirror: run the
destructor, return the memory. The middle steps are yours. The first and last steps belong
to a single, global, invisible object: ::operator new, one heap for the whole
process.
P1105R0
§4.4, "Default heap storage" proposes making that pair optional on targets with no
heap, under the feature-test macro
__cpp_freestanding_no_default_heap, which tells you how the committee sees it:
as a feature a freestanding build may switch off, rather than as something a target either
has or does not.
2. Measured: What new Requires From the Environment
A trivial-looking heap allocation, compiled to an object file and then linked with
-nostdlib:
#include <new>
struct S { int v; };
S *make(int x) noexcept(false) { return new S{x}; }
int main() {
S *p = make(7);
int r = p->v;
delete p;
return r;
}
$ g++ -std=c++26 -O2 -c nd.cc && nm -u nd.o
U _Znwm
$ g++ -std=c++26 -O2 -nostdlib nd.cc -o nd
undefined reference to `operator new(unsigned long)
One symbol, and it must come from somewhere. On a hosted system it comes from libc, which
gets the memory from sbrk/mmap, which is the kernel. On a
freestanding target, nobody wrote it. Now compare placement new, which is the
part of new that does not allocate:
#include <new>
struct S { int v; };
int f(int x) noexcept {
alignas(S) unsigned char buf[sizeof(S)];
S *p = new (buf) S{x};
return p->v;
}
$ g++ -std=c++26 -O2 -nostdlib -c pn.cc -o pn.o && nm -u pn.o
[0 undefined symbols]
Zero. Placement new is a pure language feature: construct an object at an
address you already have. It is the only form of new that a freestanding
program can use without a heap, and it is the form that the allocator model in
19.9 is built on.
3. The Mess, in Five Parts
Even on targets that do have memory, new is the worst-designed part of
the language, and the freestanding story is only one of the reasons.
-
One global heap, chosen for you. There is exactly one
::operator new. Replacing it replaces the heap of every library in the process, including ones you do not control. -
The failure policy is invisible. Whether allocation failure throws
bad_alloc, terminates, or returnsnullptrdepends on which overload you called and how your program was built. Nothing in a signature says so (19.2 argues that termination is the only correct policy). -
It is a language keyword bound to a link-time symbol. You cannot pass a
heap to a function that uses
new. The function either allocates from the global heap or it does not allocate. -
A virtual destructor silently requires
operator delete. P1105R0 says it plainly: the class's vtable points at a deleting destructor that calls::operator delete, so merely having a virtual destructor places a burden on freestanding users who never callnew. That is how a hello-world interface ends up depending on a heap. -
Arrays, alignment and nothrow multiply it.
new T[n]needs an array cookie andoperator new[]; over-aligned types need the aligned overloads;std::nothrowis a fourth variant of the same thing (18.7 covers the class-level versions).
4. And Even If It Were Clean, One Heap Is Not Enough
This is the part that C++ programmers usually miss, and kernel programmers never do. A kernel does not have "a" heap. It has one per memory zone, one per NUMA node, one per device with DMA constraints, plus pools with different lifetimes and different accounting. Choosing which heap an allocation comes from is a semantic decision, not a performance one, and it cannot be expressed by a single global.
So the question is not "is new dangerous"; it is "how does the allocation
source appear in the type system". Here it is, done the other way. Two arenas, two
containers, one program — the allocator is a template argument, so each container can
only ever use its own heap:
#include <fast_io.h>
#include <fast_io_dsal/vector.h>
template <::std::size_t extent>
class arena {
public:
alignas(::std::max_align_t) static inline ::std::byte storage[extent]{};
static inline ::std::size_t used{};
static ::fast_io::allocation_least_result
allocate_aligned_conditional_zero_at_least_die(::std::size_t alignment,
::std::size_t n,
bool zero) noexcept {
::std::size_t const start{(used + alignment - 1u) / alignment * alignment};
if (start + n > extent) { ::fast_io::fast_terminate(); }
used = start + n;
if (zero) {
for (::std::size_t i{}; i != n; ++i) { storage[start + i] = ::std::byte{}; }
}
return {storage + start, n};
}
static void deallocate(void *) noexcept {}
static void deallocate_aligned_n(void *, ::std::size_t, ::std::size_t) noexcept {}
};
using heap_a = ::fast_io::generic_allocator_adapter<arena<4096>>;
using heap_b = ::fast_io::generic_allocator_adapter<arena<8192>>;
a from A true b from B true
a from B false b from A false
A used 144 of 4096
B used 144 of 8192
Neither container can reach the other's arena: they are different types, and the allocator
is part of the type. There is no global heap in the picture, no
::operator new, no way to misconfigure it at run time, and the failure policy
is written in the arena — fast_terminate() here, or a herbception if the
arena had used the _try flavour.
5. What Should Happen, and What This Book Does
P1105R0's proposed rule is the right one: on a system without a default heap, the
replaceable allocation functions are simply not provided, and a program that uses
new fails to link — at compile time, not at 3 a.m. on the board. It also
notes why the alternatives do not work: an ::operator new that immediately
throws bad_alloc "would require pulling in all the exception handling
machinery", which is exactly the deal you were trying to avoid.
new / delete | fast_io allocator | |
|---|---|---|
| Which heap | One global, chosen at link time | A template argument, chosen per container |
| Failure policy | Depends on the overload and the build | A flag in the allocator type (19.9) |
| Can a function allocate from a heap it was not given? | Yes | No |
| Freestanding | No default heap (P1105R0) | Yes, the arena above compiles with zero external symbols |
| Swappable per subsystem | Only by replacing the global | By instantiating a different type |
The rule to take away: never write new in your own code, and
do not design interfaces that allocate behind the caller's back. When you need storage, say
where it comes from — a buffer, an arena, an allocator type — and the program
will work on a board, in a kernel, and on a desktop without changing a line.