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 会重试,因此失败条件不变时会忙循环。

behavior of the new-handler失败不是终点:handler 的返回值决定是否进入下一轮allocationrequest storagefirst attempt failsset_new_handlerread current hookcall if presenthandler actionrelease / switchthrow / terminateretry or bad_allocchanged condition → retryno progress → bounded exitclass-specific new-handlerRAII 安装 / 恢复;并发时仍要警惕 global statehandler 返回意味着“我改变了下一次尝试的条件”,而不是“继续无条件重试”
把 new-handler 当作一个有限状态转换:每次返回都必须能解释下一轮为何更有机会成功,或为何会到达明确失败边界。

失败路径实验

先预测:handler 返回后会发生什么?

切换三种故障场景,观察 handler 如何改变下一次 allocation,以及 RAII 为什么只能解决恢复而不能自动解决并发隔离。

观察

请求第一次失败;handler 释放预留 storage,再返回给 throwing allocation function。

决策

返回前必须让下一次尝试看到不同条件;若 reserve 仍不足,就把后继策略设为抛出。

当前场景 · set_new_handler / release reserve

返回前必须让下一次尝试看到不同条件;若 reserve 仍不足,就把后继策略设为抛出。

handler 必须改变条件或离开控制流

有效 handler 通常只能做以下几类事情。

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。

handler 应使用预分配数据、固定 buffer、低层无分配输出,逻辑保持短小。它还可能在任意 allocation site 被调用,不应假设具体业务上下文。

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。

测试内存不足不能只靠真实耗尽

真实 OOM 不稳定,还可能触发操作系统 overcommit 或杀进程。应为 allocation layer 加可控 failure injection。

测试矩阵包括:无 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 仍可抛;临时全局安装还需处理线程竞争

资料与写作方式声明

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

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

名词解释

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

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. 问题 1:设计一个释放 reserve 后只重试一次的 handler。 若仍失败应抛 bad_alloc。
  1. 问题 2:实现 Widget class-specific new-handler。 global allocation 成功或抛异常后都必须恢复旧策略。
  1. 问题 3:两个线程分别分配 Widget 与 Image,各有不同 class handler。 审查设计。

讨论

评论区加载中…