第18.7章 类级 operator new 与 operator delete

English

1. 当一个类拥有自己的分配

类可以通过声明一个名为 operator new 的静态成员, 替换作用在它自己身上的 new 所使用的分配。 当你写 new T(...) 时,编译器先找 T::operator new, 只有类里没有时才退回全局那个。

你必须想清楚的一整套:

形式 被谁调用 需要吗
static void *operator new(::std::size_t) new T 要,只要你写了其中任何一个
static void operator delete(void *) 不带大小的 delete p 要 —— 缺了它,全局那个会被用来释放你自己分配的内存
static void operator delete(void *, ::std::size_t) delete p(C++14 起的带大小释放) 要 —— 编译器通常调用的就是它
static void *operator new[](::std::size_t) new T[n] 只有当你也想给数组用池时
static void *operator new(::std::size_t, void *) 定位 new (p) T 永远不要:全局的定位形式不允许被替换

不匹配是经典缺陷:一个类声明了 operator new 却忘了 operator delete,于是它的内存从你的池里来、却还给 C++ 的堆。 那是堆破坏,没有任何诊断,通常表现为一秒之后某个不相干的分配里崩溃。

2. 一个完整正确的例子


#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

要注意五件事,每一件都是一条规则:

3. 分配失败就是终止

堆分配失败必须终止程序。不是“被处理”、不是“被报告”、不是“被重试”: 而是终止,就地终止,就在内存耗尽的那个地方。这一页之所以存在, 就是因为 C++ 选了相反的做法。

把堆分配失败做成异常,是 C++ 历史上最大的错误之一。 想想这门语言承诺了什么:拿不到内存是一个调用者能从中恢复的异常情形, 它通过一条带类型的通道传播,而整个程序的不变量在它穿过之后仍然成立。这些统统不成立。 栈耗尽是未定义行为,所以这两种失败模式不可能都被“处理”。 从分配失败里退栈会跑析构函数,而析构函数又会分配。 你下面的库本来就直接 abort:glibc 的 xmalloc 不返回空,它直接死。 在 Linux 上,OOM killer 会在 malloc 返回空之前先到。 而且程序里每个 new 都变成一条能离开任何函数的看不见的控制流边, 这就是那些看起来根本没有错误处理的代码里异常安全 bug 的来源。

所以我们要说 1998 年就该说的那句话:分配失败就 abort。 真发生了,程序就结束了,诊断就是一份 core dump,而这是诚实的结果。 其他做法全是演戏,代价是每个调用者一次检查、每个函数一条看不见的出口。

内存不在时,一个分配函数总共只有四种做法,而其中只有一种值得写:

它做什么 结论
抛 ::std::bad_alloc 语言的默认行为。一个 1998 年的遗留异常,在赫布异常程序里会以 [itanium_exception](St9bad_alloc) 的形式被拖进来 (第18.1章)
返回 nullptr 永远不要。它之所以合法,只是因为这个函数是 noexcept 的,而这时 new T 得到的是一个空指针: 每个调用者都必须检查,而没有调用者会检查,对象也从未被构造
::std::nothrow / ::std::nothrow_t 永远不要 —— 见下
::std::abort() 这一个

为什么 ::std::abort() 才是正确答案

来自 Portable-Cpp-Guideline 的五个理由,对一个不是内核的程序全都成立:

  1. 析构函数可能再次分配。从分配失败里退栈会跑析构函数, 而一个会分配的析构函数也可能失败,于是“处理它”这条路是一个没有保证出口的循环。
  2. libsupc++ 本来就有紧急堆,而即使有它,也有很多路径最终走到 ::std::terminate。把紧急堆删掉试过,可观察行为没有任何变化。
  3. 你下面的库也不处理它。glibc 调 xmalloc, 而 malloc 失败时它就 abort。你没法让自己的程序的失败故事比 C 库更优雅。
  4. Linux 会 overcommit。在 malloc 返回空之前, 你通常已经先被 OOM killer 杀掉了,所以你精心写下的那个分支, 在你真正发布的平台上就是死代码。
  5. 程序里每个 new 都是一条看不见的控制流边 (第18.1章); 把分配失败做成致命的,一次性把这些边全部消掉。

指南还明确给出一个一致性论证:在 C++ 里栈耗尽是未定义行为,而堆耗尽不是。 这两种失败模式不可能都被“处理”,所以把一个当成致命的、另一个当成可恢复的,是演戏。

永远不用 ::std::nothrow

::std::nothrow 看着像中间路线,其实不是:libstdc++ 的 nothrow operator new 就是在 try/catch(...) 里调用会抛的那个, 任何异常都返回空:


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

所以 ::std::nothrow 对“会不会抛”什么保证都没有 —— 抛还是抛了,只是替你接住了 —— 而标准不允许你像替换 operator new() 那样替换 operator new(std::nothrow_t const&)。 它没有任何好处,还少了一样你能控制的东西。不要用它,也不要把它传给 new:如果你要一条不会失败的分配路径,就写 malloc 并检查它; 而对失败你又做不了任何有意义的事,就调用 ::std::abort()。

4. 为什么你不该这么做

5. 那该做什么

目标 类 operator new 更好的做法
让某个热点类型分配得更快 static void *operator new(size_t) 一个传给容器的池对象,或者一个区域类型 (第14.1章)
分配时不抛异常 返回空的 operator new,或者 ::std::nothrow 在你自己的类型里用 malloc,失败时 ::std::abort(),而真正算错误的东西用 throw throws 报告 (第18.6章)
批量生命周期 一个泄露的 operator delete 一个带析构函数的区域类型,一次调用释放全部
诊断 在 operator new 里放计数器 测试构建里给 ::operator new 做一个链接期插桩,或者用消毒器

6. 经验法则