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.

  1. 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.
  2. The failure policy is invisible. Whether allocation failure throws bad_alloc, terminates, or returns nullptr depends 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).
  3. 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.
  4. 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 call new. That is how a hello-world interface ends up depending on a heap.
  5. Arrays, alignment and nothrow multiply it. new T[n] needs an array cookie and operator new[]; over-aligned types need the aligned overloads; std::nothrow is 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 / deletefast_io allocator
Which heapOne global, chosen at link timeA template argument, chosen per container
Failure policyDepends on the overload and the buildA flag in the allocator type (19.9)
Can a function allocate from a heap it was not given?YesNo
FreestandingNo default heap (P1105R0)Yes, the arena above compiles with zero external symbols
Swappable per subsystemOnly by replacing the globalBy 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.