Item 52:写了 placement new 也要写 placement delete
对齐 Effective C++ 第三版 Item 52:解释 placement new 后 constructor exception 如何查找 matching placement delete,区分构造失败回收与普通 delete expression,并恢复被 class-specific overload 隐藏的 normal forms。
学习目标
- 能解释 placement new 取得 storage 后 constructor 抛异常时,compiler 如何寻找 matching placement delete
- 能实现 normal、standard placement、nothrow 与自定义 logging forms 的签名配对并验证无泄漏
- 能分析 class-specific operator new/delete 的 name hiding,并用完整 overload set 或 using declaration 恢复 normal forms
构造失败配对实验
placement new 的 cleanup 选择
先预测 new expression 的 allocation、construction 和 cleanup,再切换场景查看证据。
allocation / construction
placement new(size, log) 已取得 raw storage,constructor 尚未完成。
cleanup 规则
按额外参数匹配 placement delete(void*, log),记录失败并归还 storage。
当前场景 · constructor exception
按额外参数匹配 placement delete(void*, log),记录失败并归还 storage。
从构造函数抛异常开始
class Widget {
public:
static void* operator new(std::size_t size, std::ostream& log);
};
Widget* widget = new (std::cerr) Widget;这里的 allocation function 除 size 外还接收 log 参数,因此属于 placement new 的广义形式。
↡除 size 外还接收一个或多个额外参数的 operator new allocation function。 ↡参数列表与某个 placement new 额外参数对应、供构造失败回收 storage 的 operator delete。Item 52 的原则是 Write placement delete if you write placement new(写了 placement new 就写 placement delete)。
先预测:storage 分配成功后 Widget constructor 抛异常,谁有 pointer、谁负责释放?new expression 必须自动寻找与所用 allocation form 匹配的 delete。
new expression 是 allocation 加 construction
1. operator new(size, log) -> raw storage
2. Widget constructor runs in that storage
3. return Widget pointer若第二步失败,对象从未完成,客户也拿不到 pointer,无法手工 delete。语言需要自动回收第一步 storage。
↡allocation 已成功但 object construction 未完成并抛出异常的路径。 ↡构造失败后没有匹配 deallocation function,已取得 raw storage 无法自动归还。matching placement delete 的签名
class Widget {
public:
static void* operator new(std::size_t size, std::ostream& log);
static void operator delete(void* memory, std::ostream& log) noexcept;
};placement delete 的第一个参数是要释放的 pointer,后续参数类型与 placement new 除 size 外的参数匹配。
↡placement new 的额外参数序列在 placement delete 中按对应类型重现的规则。void Widget::operator delete(void* memory, std::ostream& log) noexcept {
log << "constructor failed\n";
::operator delete(memory);
}constructor 抛出后,语言可调用这条函数;若只有普通 operator delete(void*),它不会被当成该 placement allocation 的构造失败匹配。
placement delete 不是正常 delete expression 的目标
Widget* widget = new (std::cerr) Widget;
delete widget;若 construction 成功,后续普通 delete widget 调用普通 class-specific/global delete,不会保存并重放最初的 log argument。
因此 class 仍需普通 form:
static void operator delete(void* memory) noexcept;
static void operator delete(void* memory, std::ostream& log) noexcept;第一条服务正常 delete,第二条只服务 logging placement construction failure。
↡placement delete 仅在对应 new expression 构造失败时由语言自动考虑的作用域。standard placement new 的特殊语义
标准 placement form 在调用者提供的 buffer 上构造:
alignas(Widget) std::byte storage[sizeof(Widget)];
Widget* widget = new (storage) Widget;其 allocation function 概念上返回传入 pointer,不拥有 buffer。matching operator delete(void*, void*) 在 constructor failure 时通常无操作,因为 storage 属于调用者。
调用者必须显式调用 destructor,不能对 stack buffer 使用普通 delete。
多种 allocation forms 要逐一配对
normal、nothrow、aligned、自定义 log/context forms 都有各自 overload selection 和 failure matching。现代 compiler 还可能使用 sized/aligned delete;实现完整 class allocator 时需整体审计。
↡同一 class 提供的 normal、placement、nothrow、sized 与 aligned allocation/deallocation 函数集合。class scope 会隐藏外层同名 forms
只声明 logging operator new:
class Widget {
public:
static void* operator new(std::size_t, std::ostream&);
};
// new Widget; // normal form may be hidden
// new (buffer) Widget; // standard placement form may be hidden这与普通函数 hiding 相同:编译器按 name 找到 class scope 后,不会自动把 global overloads 混入候选。
方案一:显式提供完整标准 forms
class Widget {
public:
static void* operator new(std::size_t size) {
return ::operator new(size);
}
static void* operator new(std::size_t, void* memory) noexcept {
return memory;
}
static void* operator new(std::size_t size, const std::nothrow_t& tag) noexcept {
return ::operator new(size, tag);
}
static void* operator new(std::size_t size, std::ostream& log);
};每条 allocation 还需对应 deallocation。优点是 class API 自包含,缺点是样板多且容易漏新语言 forms。
↡在 class 中转发 global standard overloads,同时添加自定义 placement pair 的方案。方案二:标准形式基类加 using
class StandardNewDeleteForms {
public:
static void* operator new(std::size_t size) { return ::operator new(size); }
static void operator delete(void* p) noexcept { ::operator delete(p); }
static void* operator new(std::size_t, void* p) noexcept { return p; }
static void operator delete(void*, void*) noexcept {}
};
class Widget : public StandardNewDeleteForms {
public:
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
static void* operator new(std::size_t, std::ostream&);
static void operator delete(void*, std::ostream&) noexcept;
};真实实现还要覆盖 nothrow/aligned/sized forms,并验证访问级别。基类只是组织样板,不改变每个 pair 的契约。
placement 参数的生命周期与异常安全
placement delete 在 constructor unwind 中接收原 arguments。若参数是 reference/pointer,它们必须在调用期间仍有效;handler 不能假设完整 Widget 已存在。
↡placement extra arguments 在 allocation、construction 和 failure cleanup 全程保持有效的要求。logging delete 自己必须 noexcept,且错误日志路径最好不动态分配,否则原 constructor exception unwind 中又触发失败。
↡placement delete 在处理原构造异常时不得再抛出第二个异常的约束。不是所有带额外参数的 new 都拥有 storage
custom arena/context placement new 可能从 arena 分配,也可能只定位已有 buffer。matching delete 的行为必须与 ownership 对称:拥有 block 就归还,borrowed buffer 就 no-op/回滚 cursor。
↡placement allocation 是否取得并负责归还 storage 的明确策略。static void* operator new(std::size_t size, Arena& arena);
static void operator delete(void* p, Arena& arena) noexcept;arena placement delete 可调用 arena.deallocate(p),前提是 arena 支持单块回收;monotonic arena 则可能只记录失败而不回收。
验收矩阵
- 每个 placement new 都有 exact matching placement delete。
- constructor 成功后 normal delete 使用 normal form。
- constructor 每个阶段抛异常时 storage 计数归零。
- standard placement buffer 不被错误 free,且显式 destructor 正确。
- class normal/void-pointer/nothrow/aligned forms 不因 hiding 消失。
- extra arguments 生命周期覆盖 failure cleanup,delete 不抛。
先预测每个 new expression 在 allocation 和 constructor failure 时分别选择哪两个 functions,再用计数 allocator 与故意抛出的 constructor 验证。
小结
- placement new 是任何带额外参数的 allocation function,不只标准 void-pointer form
- constructor 抛异常时,语言按额外参数寻找 matching placement delete;缺失会泄漏 storage
- placement delete 只服务构造失败,成功对象的普通 delete expression 走 normal delete
- class-specific operator new/delete declarations 会隐藏同名 normal forms
- 显式 forwarding 或标准基类加 using 可恢复完整 overload family
- placement delete 必须与 storage ownership 对称、参数有效且 noexcept
名词解释
本章出现的专业名词,用大白话再讲一遍。
- placement new
除 size 外带额外参数的分配函数。
- placement delete
匹配 placement new 的构造失败释放函数。
- constructor exception
storage 成功但对象构造失败。
- constructor-failure memory leak
缺匹配 delete 导致的 storage 泄漏。
- placement signature matching
额外参数类型在 delete 中对应重现。
- normal deallocation path
成功对象由普通 delete 释放的路径。
- placement-delete failure scope
placement delete 仅服务构造失败。
- standard placement form
在调用者 void pointer storage 上构造。
- caller-owned placement storage
placement 不拥有外部 buffer。
- allocation overload family
各种 new/delete forms 的完整集合。
- allocation name hiding
class 声明隐藏外层同名分配函数。
- standard-form forwarding
class 显式转发标准 global forms。
- allocation overload re-exposure
using 引回 base overload set。
- placement argument lifetime
额外参数覆盖 cleanup 的寿命要求。
- cleanup no-throw boundary
失败清理函数不再抛异常。
- placement storage ownership
placement 路径的 storage 所有权策略。
- placement allocation matrix
成功、失败和查找的测试表。
练习
- 问题 1:logging placement new 已取得 storage,Widget constructor 抛出(placement new、placement delete、constructor exception)。 设计 matching delete。
- 问题 2:新增 logging new 后,普通 new Widget 与标准 placement new 都不再编译(name hiding、normal forms)。 修复名称查找。
- 问题 3:Arena placement new 在 monotonic arena 分配,constructor 失败(placement delete、memory leak、placement storage ownership)。 placement delete 应怎样处理?