Item 49:理解 new-handler 的行为
对齐 Effective C++ 第三版 Item 49:解释 operator new 内存不足时调用 new-handler 并重试的循环,列出 handler 的合法退出策略,并用 RAII 与 class-specific operator new 安全安装和恢复处理器。
学习目标
- 能解释 operator new 内存不足时读取、调用 new-handler 并重复 allocation 的完整控制流
- 能实现不会空转的 handler,在释放 reserve、切换策略、抛 bad_alloc 与终止之间做明确选择
- 能回答:nothrow new 为什么仍可能抛异常,以及临时切换 global handler 在并发下有什么边界?
先想象一个快空的仓库
程序突然需要一块暂存空间,但仓库只剩最后一点余量。一个可靠的管理员不会只说“再试一次”:他要么腾出预留位置,要么把请求交给下一套规则,要么明确宣布无法继续。
如果管理员什么也没改变却反复放行同一个请求,程序就会在失败和重试之间打转。这个类比正是本章要建立的判断:失败处理必须改变条件,或离开当前控制流。
从 allocation 失败之后开始
void* memory = ::operator new(hugeSize);当底层无法满足请求,throwing operator new 不一定立刻抛异常。它先查看当前 new-handler;若存在便调用它,然后重试 allocation。
Item 49 的原则是 Understand the behavior of the new-handler(理解 new-handler 行为)。
using Handler = std::new_handler; // void (*)()
Handler old = std::set_new_handler(handleOutOfMemory);先预测:handler 若什么也不做直接返回,会发生一次 bad_alloc 还是无限调用?throwing operator new 会重试,因此失败条件不变时会忙循环。
失败路径实验
先预测:handler 返回后会发生什么?
切换三种故障场景,观察 handler 如何改变下一次 allocation,以及 RAII 为什么只能解决恢复而不能自动解决并发隔离。
观察
请求第一次失败;handler 释放预留 storage,再返回给 throwing allocation function。
决策
返回前必须让下一次尝试看到不同条件;若 reserve 仍不足,就把后继策略设为抛出。
当前场景 · set_new_handler / release reserve
返回前必须让下一次尝试看到不同条件;若 reserve 仍不足,就把后继策略设为抛出。
handler 必须改变条件或离开控制流
有效 handler 通常只能做以下几类事情。
↡预留到紧急时刻才释放、为关键失败路径提供最后少量可用 storage 的内存。std::unique_ptr<std::byte[]> reserve;
void releaseReserve() {
reserve.reset();
}释放 reserve 让下一次 allocation 可能成功。handler 也可安装另一个 handler,形成降级链;或把 handler 设为 null,让下一次失败抛 std::bad_alloc。
handler 可直接 throw bad_alloc 或其派生异常;若系统不可恢复,也可 terminate/abort。关键是不在条件未变时正常返回。
handler 自己不能依赖普通动态分配
out-of-memory 路径中再构造 string、扩展 vector、格式化堆日志或锁一个会分配的 tracing system,都可能递归进入同一 handler。
↡new-handler 内部再次动态分配并触发自身,导致递归失败或死循环的风险。handler 应使用预分配数据、固定 buffer、低层无分配输出,逻辑保持短小。它还可能在任意 allocation site 被调用,不应假设具体业务上下文。
↡失败处理路径预先准备资源,运行时不再依赖可能失败的 allocator。set_new_handler 操作的是全局状态
auto previous = std::set_new_handler(releaseReserve);
// allocations now observe releaseReserve
std::set_new_handler(previous);手工恢复容易在 exception、early return 或后续维护中遗漏。应使用 RAII。
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler handler) noexcept
: old_(std::set_new_handler(handler)) {}
~NewHandlerHolder() { std::set_new_handler(old_); }
NewHandlerHolder(const NewHandlerHolder&) = delete;
NewHandlerHolder& operator=(const NewHandlerHolder&) = delete;
private:
std::new_handler old_;
};class-specific new-handler
标准只有全局 handler API,但 class 可在自己的 operator new 中临时安装专属策略。
class Widget {
public:
static std::new_handler setNewHandler(std::new_handler handler) noexcept {
return std::exchange(currentHandler_, handler);
}
static void* operator new(std::size_t size) {
NewHandlerHolder guard(currentHandler_);
return ::operator new(size);
}
private:
static inline std::new_handler currentHandler_ = nullptr;
};Widget::operator new 安装 Widget handler,委托 global ::operator new 执行标准重试循环;返回或抛异常时 guard 恢复原 handler。
用 CRTP 复用 class 支持
多个 classes 需要相同机制时,可抽成 mixin。
template<class T>
class NewHandlerSupport {
public:
static std::new_handler setNewHandler(std::new_handler h) noexcept {
return std::exchange(handler_, h);
}
static void* operator new(std::size_t size) {
NewHandlerHolder guard(handler_);
return ::operator new(size);
}
private:
static inline std::new_handler handler_ = nullptr;
};
class Widget : public NewHandlerSupport<Widget> {};每个 T specialization 拥有自己的 handler,不会把 Widget 与 Image 的 policy 混在同一个 static variable。
operator new 与 new expression 不同
new Widget 包含两步:先调用 allocation function 取得 raw storage,再调用 Widget constructor。new-handler 只处理第一步内存取得失败。
constructor 抛出的业务异常不会触发 new-handler;语言会调用匹配 delete 释放刚取得的 storage,然后传播 constructor exception。
nothrow new 的边界
Widget* widget = new (std::nothrow) Widget;nothrow 只约束 allocation failure 的最终表现,不保证整个 new expression 不抛:Widget constructor 仍可抛异常。且底层 nothrow allocation 的具体 handler 交互应按实现与标准契约测试,不能把它当成无失败路径。
class-specific 临时安装有并发风险
std::set_new_handler 修改 global state。线程 A 为 Widget 临时安装 handler 时,线程 B 的无关 allocation 也可能观察到它;多个 class allocation 并发更会相互覆盖。
简单 mutex 若覆盖 global handler 作用域可序列化安装,但 handler/allocator 与锁的交互需避免 deadlock。现代系统更常把 class-specific failure policy放入显式 allocator/resource,而不是频繁切换 process-global hook。
↡通过对象或参数显式传递 allocation policy,避免修改进程全局 handler 的架构。测试内存不足不能只靠真实耗尽
真实 OOM 不稳定,还可能触发操作系统 overcommit 或杀进程。应为 allocation layer 加可控 failure injection。
↡在指定 allocation 次数或大小主动返回失败,用来稳定覆盖 handler 路径的测试机制。测试矩阵包括:无 handler 抛 bad_alloc;handler 释放 reserve 后重试成功;handler 直接抛;class handler 异常后恢复旧 handler;constructor exception 与 allocation failure 分离;并发策略不串线。
先预测每个 handler 被调用次数和最终 global handler,再执行 injection test,防止意外无限循环。
小结
- throwing operator new 失败时调用当前 new-handler,handler 返回后会重试 allocation
- handler 必须释放资源、切换/卸载 handler、抛异常或终止,不能在条件不变时返回
- failure path 应避免普通动态分配,防止递归进入 handler
- set_new_handler 操作 global state,RAII guard 保证成功和异常路径恢复
- class-specific operator new 可临时安装专属 handler并委托 global operator new
- nothrow 只处理 allocation failure,constructor 仍可抛;临时全局安装还需处理线程竞争
名词解释
本章出现的专业名词,用大白话再讲一遍。
- new-handler
allocation 失败时调用的处理函数。
- std::set_new_handler
安装并返回全局处理器的函数。
- out of memory
无法取得请求 storage 的状态。
- emergency memory reserve
失败时释放的预留内存。
- std::bad_alloc
throwing allocation 的失败异常。
- allocation recursion hazard
handler 内分配再次触发自身的风险。
- allocation-free failure path
不依赖普通 allocator 的失败路径。
- global handler state
所有 allocation 可观察的处理器状态。
- new-handler RAII guard
作用域安装并恢复 handler 的对象。
- class-specific new-handler
只服务某类 allocation 的策略。
- global allocation delegation
class new 转交全局 new。
- CRTP new-handler support
按 derived class 生成独立 handler 的 mixin。
- new expression
分配 storage 并构造对象的完整表达式。
- allocation function
只取得 raw storage 的函数。
- nothrow new
allocation 失败返回 null 的形式。
- handler installation race
并发切换全局 handler 的竞争。
- allocator-scoped failure policy
显式绑定 allocator 的失败策略。
- allocation failure injection
可控触发分配失败的测试机制。
练习
- 问题 1:设计一个释放 reserve 后只重试一次的 handler。 若仍失败应抛 bad_alloc。
- 问题 2:实现 Widget class-specific new-handler。 global allocation 成功或抛异常后都必须恢复旧策略。
- 问题 3:两个线程分别分配 Widget 与 Image,各有不同 class handler。 审查设计。