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
new / delete 机制:两阶段操作new 表达式(分配 + 构造)operator new分配原始内存构造函数在内存上构造对象返回指针T* p = new T构造失败 → operator delete 自动回收内存delete 表达式(析构 + 释放)析构函数清理对象资源operator delete释放原始内存内存归还delete pplacement new(在已有内存上构造)已有内存char buf[sizeof(T)]placement newnew (buf) T()对象就位不分配,只构造new = operator new(分配) + 构造;delete = 析构 + operator delete(释放)条款 49-52:new-handler 行为、定制时机、固守常规、placement new/delete 配对
new/delete 机制:new 分两阶段(operator new 分配内存 → 构造函数),delete 反向(析构函数 → operator delete 释放),placement new 在已有内存上只构造不分配。

构造失败配对实验

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 的广义形式。

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。

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 外的参数匹配。

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。

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 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。

方案二:标准形式基类加 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 已存在。

logging delete 自己必须 noexcept,且错误日志路径最好不动态分配,否则原 constructor exception unwind 中又触发失败。

不是所有带额外参数的 new 都拥有 storage

custom arena/context placement new 可能从 arena 分配,也可能只定位已有 buffer。matching delete 的行为必须与 ownership 对称:拥有 block 就归还,borrowed buffer 就 no-op/回滚 cursor。

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 则可能只记录失败而不回收。

验收矩阵

  1. 每个 placement new 都有 exact matching placement delete。
  2. constructor 成功后 normal delete 使用 normal form。
  3. constructor 每个阶段抛异常时 storage 计数归零。
  4. standard placement buffer 不被错误 free,且显式 destructor 正确。
  5. class normal/void-pointer/nothrow/aligned forms 不因 hiding 消失。
  6. 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

资料与写作方式声明

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

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

名词解释

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

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. 问题 1:logging placement new 已取得 storage,Widget constructor 抛出(placement new、placement delete、constructor exception)。 设计 matching delete。
  1. 问题 2:新增 logging new 后,普通 new Widget 与标准 placement new 都不再编译(name hiding、normal forms)。 修复名称查找。
  1. 问题 3:Arena placement new 在 monotonic arena 分配,constructor 失败(placement delete、memory leak、placement storage ownership)。 placement delete 应怎样处理?

讨论

评论区加载中…