Item 51:编写 new 和 delete 时遵守常规
对齐 Effective C++ 第三版 Item 51:实现 zero-byte 与 new-handler 约定,防止 class-specific operator new 截断 derived allocation,并让 delete 遵守 null、noexcept、size/alignment 与 allocation family 配对。
学习目标
- 能实现处理 zero-byte request、new-handler 重试与 bad_alloc 的 throwing operator new 骨架
- 能解释 base class operator new 为何会接收 derived size,并设计安全的 size mismatch delegation
- 能比较 scalar、array、sized、aligned delete forms,验证 null、noexcept 和 allocator identity 配对
new/delete 契约实验
先预测:失败或派生尺寸会走哪条路径?
先预测 allocator domain、重试边界和 matching delete,再切换场景查看证据。
观察
先预测 operator new(0) 的成功契约:throwing form 仍需返回可释放的 non-null storage,而不是把零请求变成静默失败。
决策
先做 zero byte request normalization,再走与普通请求相同的对齐、失败和 matching delete 路径。
当前场景 · convention when writing new and delete / zero byte request
先做 zero byte request normalization,再走与普通请求相同的对齐、失败和 matching delete 路径。
从一份最小契约开始
void* operator new(std::size_t size);
void operator delete(void* memory) noexcept;allocation function 只取得 raw storage,deallocation function 只归还 storage。它们位于语言 new/delete expression 的底层,必须模仿标准约定。
↡自定义 allocation/deallocation functions 必须保持的大小、失败、对齐和释放行为。 ↡调用方请求分配零个 bytes 的合法边界输入。Item 51 的原则是 Adhere to convention when writing new and delete(编写 new/delete 时遵守常规)。
先预测:operator new(0) 是否可以成功返回 null?throwing form 成功应返回可供 matching delete 使用的非空 pointer;通常把请求提升到 1 byte。
zero-byte request 必须可处理
void* operator new(std::size_t size) {
if (size == 0) size = 1;
// allocate at least one byte
}标准不要求两个存活的零请求共享地址;最稳健做法是按最小 block 分配,使每次成功结果可独立传给 delete。
↡成功 throwing allocation 返回非空且适当对齐、可由对应 delete 释放的地址。allocation 失败要遵守 new-handler 循环
void* operator new(std::size_t size) {
if (size == 0) size = 1;
for (;;) {
if (void* memory = tryAllocate(size)) return memory;
std::new_handler handler = std::get_new_handler();
if (!handler) throw std::bad_alloc{};
handler();
}
}handler 返回意味着它声称条件已改变;allocation function 必须重试。若无 handler,throwing operator new 抛 std::bad_alloc,不能静默返回 null。
handler 返回而条件不变会无限循环
Item 49 已说明 handler 的动作。Item 51 在实现侧必须接受 handler 可能返回,并重新尝试;不能只调用一次后直接 bad_alloc,也不能忽略 null handler。
↡handler 未释放资源或改变策略却正常返回,导致 allocation 不断失败重试的状态。自定义 allocator 的 failure injection tests 应限制重试次数并观察 handler 行为,测试本身避免挂死 CI。
class-specific operator new 是 static
class Base {
public:
static void* operator new(std::size_t size);
};
class Derived : public Base {
std::array<std::byte, 1024> extra_;
};allocation 在 constructor 前发生,没有对象可动态分派。class-specific operator new 是 static-like function;Derived 未声明自己的版本时,name lookup 可能使用继承来的 Base allocation function,并传入 sizeof(Derived)。
若 Base pool 固定返回 sizeof(Base) block,会造成 Derived constructor 写越界。
size mismatch 必须转交通用 allocator
void* Base::operator new(std::size_t size) {
if (size != sizeof(Base)) {
return ::operator new(size);
}
return basePool.allocate();
}只有精确 Base size 走固定池;Derived、ABI padding 变化或其他未知请求交给 global allocator。
delete 端必须镜像来源
若 Base allocation 对 size mismatch 调 global new,sized delete 应把对应 request 交给 global delete。
void Base::operator delete(void* memory, std::size_t size) noexcept {
if (!memory) return;
if (size != sizeof(Base)) {
::operator delete(memory);
return;
}
basePool.deallocate(memory);
}不能把 global block 塞入 Base pool,也不能把 pool block 交给 global delete。
delete null 必须无操作
Base::operator delete(nullptr); // must do nothing很多底层 free 已接受 null,但自定义 header lookup 若先对 pointer 做减法,会产生错误。null check 应在最前面。
↡deallocation function 不传播异常,尤其不能在 destructor unwinding 中再次抛出。new[] 只看到 raw byte count
void* Base::operator new[](std::size_t bytes);bytes 可能包含多个 elements、实现的 array cookie 和 padding。allocation function 不知道元素数,也不应构造 objects。
class hierarchy 下 inherited array allocator 还可能接收 derived arrays。除非有可靠通用设计,class fixed-size pool 不应机械复用于 new[]。
alignment 是成功结果的一部分
普通 operator new 必须满足相应默认 alignment;over-aligned allocation 通过带 std::align_val_t 的 overload 处理。
void* operator new(std::size_t size, std::align_val_t alignment);
void operator delete(void* p, std::align_val_t alignment) noexcept;若自定义 class 只声明非 aligned form,却后来增加 over-aligned member,需重新审计 lookup 和 compiler 选择;最安全是提供完整配对或显式 delegate。
sized delete 是信息,不是盲目信任
void operator delete(void* p, std::size_t size) noexcept;size 可帮助选择 pool,但 polymorphic delete、ABI 与 compiler flags 影响何时调用。allocator metadata 仍应能识别 block 来源,不要把 sized overload 当唯一安全证据。
constructor 失败需要 matching delete
new expression 先 allocation,再 constructor。constructor 抛异常时,语言寻找与所用 operator new 匹配的 delete 释放 storage。
↡constructor 未完成时由语言调用对应 deallocation function 回收 raw storage 的路径。普通 new/delete 形状看似简单,但 placement forms 的额外参数要求 Item 52 的 placement delete;本章先建立“每个 allocation form 都有可匹配释放路径”的总原则。
验收矩阵
至少覆盖:
- size 0 返回非空且可释放。
- failure + null handler 抛 bad_alloc。
- handler 释放资源后第二次成功。
- Base 与 Derived 分别走 pool/global,并正确回收。
- delete null 不访问 metadata。
- scalar/array/aligned/sized forms 配对。
- constructor 抛出时无泄漏。
- 多线程 free list 与 handler 路径无数据竞争。
先预测每个 case 选择哪个 overload 和 allocator domain,再用计数器、sanitizer 与 failure injection 验证。
小结
- throwing operator new 要正规化 zero-byte request,成功返回 non-null,失败遵循 new-handler 重试或 bad_alloc
- handler 返回而失败条件不变会形成 infinite loop,测试必须可控注入
- base class operator new 可被 derived allocation 使用,size 不等于 Base 时必须安全 delegate
- delete null 是 no-op,deallocation 必须 noexcept 并镜像原 allocator 来源
- operator new[] 接收 raw bytes,不能假定元素数或固定 Base 大小
- scalar/array/sized/aligned 和 constructor-failure 路径都需 matching delete
名词解释
本章出现的专业名词,用大白话再讲一遍。
- new-delete convention
自定义分配释放必须保持的契约。
- zero byte request
请求零 bytes 的边界输入。
- zero-size normalization
把零请求提升为最小非零请求。
- successful allocation result
成功时非空、对齐且可释放的地址。
- new-handler retry loop
失败调用 handler 后反复重试。
- throwing allocation form
失败抛 bad_alloc 的普通 new。
- new-handler infinite loop
handler 无改变返回导致的无限重试。
- base class operator new
可被派生 new 找到的 class 分配函数。
- derived-size allocation request
基类 allocator 收到派生对象大小。
- size-mismatch delegation
未知尺寸转交 global allocator。
- allocation-source mirroring
释放回到原始 allocator 来源。
- null deallocation no-op
delete null 时立即无操作返回。
- non-throwing deallocation
释放函数不传播异常。
- array allocation cookie
实现附加的数组元素元数据。
- array raw byte request
new[] 传入的总 storage bytes。
- aligned allocation form
接收显式 alignment 的分配形式。
- allocation family pairing
new/delete 函数族兼容配对。
- sized deallocation form
释放时接收 size 的 overload。
- constructor-failure deallocation
构造异常后回收 storage 的路径。
- allocation convention matrix
完整分配常规测试集合。
练习
- 问题 1:convention when writing new and delete 与 zero byte request。 实现 throwing operator new 的失败循环,无 handler 时抛异常。
- 问题 2:base class operator new 的 size mismatch。 Base 固定池 block 为 64 bytes,Derived 为 256 bytes;Derived 没有自己的 operator new。
- 问题 3:new-handler 与 infinite loop。 自定义 allocator 要支持 arrays 与 64-byte over-aligned objects,请制定 overload 和测试。