Ch18.7: Class-Level operator new and operator delete

中文(简体)

1. When a Class Owns Its Allocation

A class can replace the allocation used by new on itself by declaring a static member named operator new. When you write new T(...), the compiler looks for T::operator new first, and only falls back to the global one if the class does not have one.

The full set you must think about:

Form Called by Required?
static void *operator new(::std::size_t) new T yes, if you write any of them
static void operator delete(void *) delete p when the delete is not sized yes — if it is missing, the global one is used on memory you allocated yourself
static void operator delete(void *, ::std::size_t) delete p (sized deallocation, C++14 and later) yes — this is the one the compiler will usually call
static void *operator new[](::std::size_t) new T[n] only if you also want arrays pooled
static void *operator new(::std::size_t, void *) placement new (p) T never: the global placement form must not be replaced

Mismatch is the classic defect: a class that declares operator new and forgets operator delete gets its memory from your pool and returns it to the C++ heap. That is heap corruption with no diagnostic, and it usually shows up as a crash in an unrelated allocation a second later.

2. A Complete, Correct Example


#include <fast_io.h>
#include <cstddef>
#include <cstdint>
#include <cstdlib>

struct pooled
{
	::std::int_least32_t v{};

	static void *operator new(::std::size_t n) noexcept
	{
		void *p{::std::malloc(n)};
		if (p == nullptr)
		{
			::std::abort();
		}
		return p;
	}

	static void operator delete(void *p, ::std::size_t) noexcept
	{
		::std::free(p);
	}

	static void operator delete(void *p) noexcept
	{
		::std::free(p);
	}
};

int main()
{
	try
	{
		::fast_io::io::println("sizeof(pooled)=", sizeof(pooled));
		pooled *p = new pooled{};
		p->v = 7;
		::fast_io::io::println("v=", p->v);
		delete p;
		::fast_io::io::println("done: no counter, no null, no bad_alloc");
	}
	catch throws (::std::error e)
	{
		::fast_io::io::perrln("error: ", e);
	}
}

sizeof(pooled)=4
v=7
done: no counter, no null, no bad_alloc

Five things to notice, each of which is a rule:

3. Allocation Failure Terminates

Heap allocation failure must terminate the program. Not "be handled", not "be reported", not "be retried": terminate, immediately, in the place where the memory ran out. This page exists because C++ chose the opposite.

Making heap allocation failure an exception is one of the biggest historical mistakes in C++. Think about what the language promised: that failing to get memory is an exceptional condition a caller can recover from, that it travels through a typed channel, and that whole-program invariants survive it. None of that is true. Stack exhaustion is undefined behaviour, so the two failure modes cannot both be "handled". Unwinding out of an allocation failure runs destructors, which allocate again. The libraries underneath you abort anyway: glibc's xmalloc does not return null, it dies. On Linux the OOM killer arrives before malloc returns null. And every new in the program becomes an invisible control-flow edge that can leave any function, which is a source of exception-safety bugs in code that looks like it has no error handling at all.

So we say what should have been said in 1998: allocation failure aborts. If it happens, the program is over, the diagnostic is a core dump, and that is the honest outcome. Everything else is theatre that costs every caller a check and every function an invisible exit.

There are exactly four things an allocation function can do when the memory is not there, and only one of them is worth writing:

What it does Verdict
throw ::std::bad_alloc the language default. A legacy exception from 1998, dragged into a herbception program as [itanium_exception](St9bad_alloc) (Ch18.1)
return nullptr never. It is legal only because the function is noexcept, and then new T yields a null pointer: every caller must check, no caller does, and the object is never constructed
::std::nothrow / ::std::nothrow_t never — see below
::std::abort() this one

Why ::std::abort() is the right answer

From Portable-Cpp-Guideline, five reasons, all of which apply to any program that is not a kernel:

  1. Destructors can allocate again. Unwinding out of an allocation failure runs destructors, and a destructor that allocates can fail too, so the "handle it" path is a cycle with no guaranteed exit.
  2. libsupc++ already has an emergency heap, and even with it there are paths that end in ::std::terminate. Removing the emergency heap has been tried and changes nothing observable.
  3. The libraries under you do not handle it either. glibc calls xmalloc, which aborts on malloc failure. You cannot make your program's failure story more graceful than the C library's.
  4. Linux overcommits. You will usually be killed by the OOM killer before malloc returns null, so the branch you carefully wrote is dead code on the platform you actually ship.
  5. Every new in the program is an invisible control-flow edge (Ch18.1); making allocation failure fatal removes all of them at once.

There is also a consistency argument that the guideline makes explicitly: stack exhaustion is undefined behaviour in C++ and heap exhaustion is not. The two failure modes cannot both be "handled", so treating one of them as fatal and the other as recoverable is theatre.

Never ::std::nothrow

::std::nothrow looks like the middle ground and is not: libstdc++ implements the nothrow operator new by calling the throwing one inside try/catch(...) and returning null on any exception:


_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz, const std::nothrow_t&) noexcept
{
  __try
    {
      return ::operator new(sz);
    }
  __catch (...)
    {
      return nullptr;
    }
}

So ::std::nothrow guarantees nothing about throwing — the throw happens and is caught for you — and the standard does not let you replace operator new(std::nothrow_t const&) the way it lets you replace operator new(). There is no benefit and one less thing you control. Do not use it, and do not pass ::std::nothrow to new: if you want a non-failing allocation path, write malloc and check it, and if you cannot do anything useful with the failure, call ::std::abort().

4. Why You Should Not Do It

5. What To Do Instead

Goal Class operator new Better
faster allocation of one hot type static void *operator new(size_t) A pool object you pass to a container, or an arena type (Ch14.1)
no exceptions from allocation operator new that returns null, or ::std::nothrow malloc inside your own type, ::std::abort() when it fails, and throw throws for everything that is a real error (Ch18.6)
bulk lifetime operator delete that leaks A region type with a destructor that releases everything in one call
diagnostics a counter in operator new A link-time interposer for ::operator new in a test build, or a sanitizer

6. Rules of Thumb