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 配对
Allocation convention / source mirror请求边界 → 失败策略 → 尺寸路由 → matching deletezero byte requestnormalize to one bytenon-null + alignednew-handlerretry or throwhandler must change stateinfinite loopno-progress guardbounded failure testbase class operator newexact size → poolother size → globalnew expressionallocate before constructallocator identitypool / global / alignedmatching deletenull no-op + noexcepttest matrixsize / array / failure每个 allocation form 都要有可匹配的释放路径;尺寸信息不是 allocator 来源的唯一证据
图把 Item 51 的核心契约压成一条可检查路径:边界输入、失败处理、继承尺寸和释放来源必须同时成立。

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 的底层,必须模仿标准约定。

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。

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。

自定义 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 应在最前面。

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。

普通 new/delete 形状看似简单,但 placement forms 的额外参数要求 Item 52 的 placement delete;本章先建立“每个 allocation form 都有可匹配释放路径”的总原则。

验收矩阵

至少覆盖:

  1. size 0 返回非空且可释放。
  2. failure + null handler 抛 bad_alloc。
  3. handler 释放资源后第二次成功。
  4. Base 与 Derived 分别走 pool/global,并正确回收。
  5. delete null 不访问 metadata。
  6. scalar/array/aligned/sized forms 配对。
  7. constructor 抛出时无泄漏。
  8. 多线程 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

资料与写作方式声明

本章以Effective C++, Third Edition, Item 51权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

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 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. 问题 1:convention when writing new and delete 与 zero byte request。 实现 throwing operator new 的失败循环,无 handler 时抛异常。
  1. 问题 2:base class operator new 的 size mismatch。 Base 固定池 block 为 64 bytes,Derived 为 256 bytes;Derived 没有自己的 operator new。
  1. 问题 3:new-handler 与 infinite loop。 自定义 allocator 要支持 arrays 与 64-byte over-aligned objects,请制定 overload 和测试。

讨论

评论区加载中…