Effective C++ 条款49:了解 new-handler 的行为

Effective C++ 条款49:了解 new-handler 的行为

set_new_handler 允许客户指定一个在内存分配无法获得满足时被调用的函数;nothrow new 是一个颇具局限的工具,因为它只适用于内存分配(operator new),后继的构造函数调用还是可能抛出异常。


一、内存分配失败会发生什么?

在 C++ 中,当我们使用 new 申请内存时,如果系统无法满足这个请求,会发生什么呢?

cpp 复制代码
int* p = new int[1000000000000L];  // 申请一个巨大的数组

在传统的 C++(1993 年之前),operator new 在无法分配足够内存时会返回 null。但从现代 C++ 开始,operator new 默认会抛出 std::bad_alloc 异常。

但是,在抛出异常之前,C++ 提供了一个"补救机会"------new-handler。


二、new-handler 基础

2.1 什么是 new-handler?

new-handler 是一个由用户指定的函数,当 operator new 无法满足内存分配请求时,它会在抛出 bad_alloc 异常之前被调用。

2.2 设置 new-handler

标准库在 <new> 头文件中提供了相关接口:

cpp 复制代码
namespace std {
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) noexcept;  // C++11 起用 noexcept 替代 throw()
}
参数/返回值 说明
p 指向新 new-handler 函数的指针,传入 nullptr 表示移除当前 handler
返回值 返回之前设置的 new-handler 指针,如果没有则返回 nullptr

2.3 基本使用示例

cpp 复制代码
#include <iostream>
#include <new>
#include <cstdlib>

// 自定义的 new-handler
void outOfMemory() {
    std::cerr << "【错误】内存分配失败!无法分配请求的内存。\n";
    
    // 选择1:终止程序
    std::abort();
    
    // 选择2:抛出异常(如果不 abort)
    // throw std::bad_alloc();
}

int main() {
    // 设置自定义的 new-handler
    std::set_new_handler(outOfMemory);
    
    // 尝试分配巨量内存
    int* pBigDataArray = new int[1000000000000L];  // 触发 new-handler
    
    return 0;
}

运行输出:

复制代码
【错误】内存分配失败!无法分配请求的内存。

三、设计良好的 new-handler 应该做什么?

一个设计良好的 new-handler 函数可以考虑以下五种策略:

策略1:释放更多内存

cpp 复制代码
class MemoryPool {
    static char* emergencyBuffer;
    static constexpr size_t BUFFER_SIZE = 1024 * 1024;  // 1MB 应急内存
    
public:
    static void init() {
        emergencyBuffer = new char[BUFFER_SIZE];
    }
    
    static void releaseEmergencyBuffer() {
        delete[] emergencyBuffer;
        emergencyBuffer = nullptr;
        std::cout << "【new-handler】释放应急内存,重试分配...\n";
    }
};

char* MemoryPool::emergencyBuffer = nullptr;

void newHandler() {
    if (MemoryPool::emergencyBuffer) {
        MemoryPool::releaseEmergencyBuffer();
        return;  // 返回后 operator new 会重试分配
    }
    
    // 没有更多内存可释放,只能终止或抛异常
    std::cerr << "【new-handler】无可用内存,终止程序。\n";
    std::abort();
}

策略2:安装另一个 new-handler

cpp 复制代码
void aggressiveNewHandler();  // 更激进的内存回收策略
void conservativeNewHandler(); // 保守策略

void conservativeNewHandler() {
    std::cerr << "【保守策略】尝试轻度回收...\n";
    
    // 如果保守策略无效,切换到激进策略
    std::set_new_handler(aggressiveNewHandler);
}

void aggressiveNewHandler() {
    std::cerr << "【激进策略】尝试深度回收...\n";
    
    // 释放所有缓存
    // 如果还是不行,只能终止
    std::abort();
}

策略3:卸除 new-handler

cpp 复制代码
void temporaryNewHandler() {
    std::cerr << "【new-handler】临时 handler 被调用,移除后抛出异常。\n";
    
    // 移除 new-handler,下次分配失败直接抛 bad_alloc
    std::set_new_handler(nullptr);
    
    // 注意:此时返回后 operator new 会重试,
    // 如果仍然失败,就会抛出 bad_alloc
}

策略4:抛出 bad_alloc 或其派生异常

cpp 复制代码
class MyMemoryException : public std::bad_alloc {
public:
    const char* what() const noexcept override {
        return "自定义内存不足异常";
    }
};

void throwingNewHandler() {
    std::cerr << "【new-handler】抛出自定义异常。\n";
    throw MyMemoryException();
}

策略5:不返回(调用 abort 或 exit)

cpp 复制代码
void fatalNewHandler() {
    std::cerr << "【致命错误】内存耗尽,程序即将终止。\n";
    
    // 记录日志...
    
    std::abort();  // 或 std::exit(1)
}

四、Class 专属的 new-handler

4.1 为什么需要?

有时候我们希望针对不同类使用不同的内存分配失败处理策略。例如:

  • 对于缓存类:释放旧缓存条目
  • 对于日志类:丢弃旧日志
  • 对于关键数据类:直接终止程序

4.2 实现 Class 专属 new-handler

C++ 标准并不直接支持 class 专属的 new-handler,但我们可以通过自定义 operator new 来实现:

cpp 复制代码
#include <iostream>
#include <new>
#include <memory>

class Widget {
public:
    // 设置 Widget 专属的 new-handler
    static std::new_handler set_new_handler(std::new_handler p) noexcept;
    
    // 自定义 operator new
    static void* operator new(std::size_t size);
    
    // 自定义 operator new[]
    static void* operator new[](std::size_t size);
    
    // 自定义 operator delete(配对使用)
    static void operator delete(void* p) noexcept;
    static void operator delete[](void* p) noexcept;
    
    Widget() { std::cout << "Widget 构造\n"; }
    ~Widget() { std::cout << "Widget 析构\n"; }
    
private:
    static std::new_handler currentHandler;
    int data[100];
};

// 初始化静态成员
std::new_handler Widget::currentHandler = nullptr;

std::new_handler Widget::set_new_handler(std::new_handler p) noexcept {
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

4.3 关键的 RAII 辅助类

为了确保全局 new-handler 能被正确恢复,我们需要一个 RAII 辅助类:

cpp 复制代码
class NewHandlerHolder {
public:
    explicit NewHandlerHolder(std::new_handler nh) noexcept
        : handler(nh) {}
    
    ~NewHandlerHolder() {
        // 析构时恢复原来的 new-handler
        std::set_new_handler(handler);
    }
    
    // 禁止拷贝
    NewHandlerHolder(const NewHandlerHolder&) = delete;
    NewHandlerHolder& operator=(const NewHandlerHolder&) = delete;
    
private:
    std::new_handler handler;
};

4.4 Widget 的 operator new 实现

cpp 复制代码
void* Widget::operator new(std::size_t size) {
    // 步骤1:安装 Widget 专属的 new-handler 作为全局 new-handler
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    
    // 步骤2:调用全局 operator new 进行实际分配
    // 如果失败,会调用 currentHandler(已被设为全局 handler)
    return ::operator new(size);
    
    // 步骤3:NewHandlerHolder 析构时自动恢复原来的全局 new-handler
}

void* Widget::operator new[](std::size_t size) {
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new[](size);
}

void Widget::operator delete(void* p) noexcept {
    ::operator delete(p);
}

void Widget::operator delete[](void* p) noexcept {
    ::operator delete[](p);
}

4.5 使用示例

cpp 复制代码
void widgetOutOfMemory() {
    std::cerr << "【Widget new-handler】Widget 内存分配失败!\n";
    std::abort();
}

void globalOutOfMemory() {
    std::cerr << "【全局 new-handler】全局内存分配失败!\n";
    std::abort();
}

int main() {
    // 设置全局 new-handler
    std::set_new_handler(globalOutOfMemory);
    
    // 设置 Widget 专属 new-handler
    Widget::set_new_handler(widgetOutOfMemory);
    
    // 分配 Widget:如果失败,调用 widgetOutOfMemory
    Widget* pw1 = new Widget;
    
    // 分配其他类型:如果失败,调用 globalOutOfMemory
    std::string* ps = new std::string;
    
    // 移除 Widget 的专属 handler
    Widget::set_new_handler(nullptr);
    
    // 再次分配 Widget:如果失败,调用全局的 globalOutOfMemory
    Widget* pw2 = new Widget;
    
    delete pw1;
    delete ps;
    delete pw2;
    
    return 0;
}

五、使用 Mixin 风格复用代码

上面的实现对于每个类来说代码几乎相同,我们可以通过 CRTP(奇异递归模板模式) 来复用:

cpp 复制代码
#include <iostream>
#include <new>

// Mixin 基类模板
template<typename T>
class NewHandlerSupport {
public:
    static std::new_handler set_new_handler(std::new_handler p) noexcept;
    static void* operator new(std::size_t size);
    static void operator delete(void* p) noexcept;
    
private:
    static std::new_handler currentHandler;
};

template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) noexcept {
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) {
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new(size);
}

template<typename T>
void NewHandlerSupport<T>::operator delete(void* p) noexcept {
    ::operator delete(p);
}

// 每个 T 都有独立的 currentHandler 实例
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = nullptr;

// ==================== 使用 ====================

class Widget : public NewHandlerSupport<Widget> {
    // 不需要声明 set_new_handler 或 operator new
    // 全部从基类继承
};

class BigData : public NewHandlerSupport<BigData> {
    double data[10000];
};

void widgetHandler() {
    std::cerr << "【Widget】释放缓存重试...\n";
    std::set_new_handler(nullptr);  // 下次直接抛异常
}

void bigDataHandler() {
    std::cerr << "【BigData】清理临时文件...\n";
    std::set_new_handler(nullptr);
}

int main() {
    Widget::set_new_handler(widgetHandler);
    BigData::set_new_handler(bigDataHandler);
    
    Widget* w = new Widget;      // 失败时调用 widgetHandler
    BigData* bd = new BigData;   // 失败时调用 bigDataHandler
    
    delete w;
    delete bd;
    
    return 0;
}

注意Widget 继承自 NewHandlerSupport<Widget>,这种"以派生类作为模板参数继承基类"的模式就是 CRTP


六、nothrow new 的局限性

6.1 什么是 nothrow new?

在 1993 年之前,new 在失败时返回 null。为了兼容旧代码,C++ 提供了 nothrow 版本:

cpp 复制代码
#include <new>

// 使用 nothrow new
Widget* pw = new (std::nothrow) Widget;

if (pw == nullptr) {
    std::cerr << "分配失败\n";
} else {
    // 使用 pw...
    delete pw;
}

6.2 局限性分析

nothrow new 有一个严重的局限性

它只保证 operator new(内存分配)不抛出异常,但后续的构造函数仍然可能抛出异常

cpp 复制代码
class DangerousWidget {
public:
    DangerousWidget() {
        // 构造函数内部又分配内存
        internalData = new int[1000000000000L];  // 可能抛出 bad_alloc!
    }
    
    ~DangerousWidget() {
        delete[] internalData;
    }
    
private:
    int* internalData;
};

// 即使使用 nothrow,构造函数仍可能抛异常
DangerousWidget* pdw = new (std::nothrow) DangerousWidget;
// 如果构造函数抛出异常,pdw 仍然会得到 nullptr 吗?
// 答案是:不会!异常会传播出来!
new 类型 内存分配失败 构造函数异常
普通 new bad_alloc 抛异常
nothrow new 返回 nullptr 仍可能抛异常

6.3 正确使用 nothrow new

nothrow new 只在以下场景有用:

  1. 分配原始内存 (如 char 数组),不涉及构造函数
  2. 与 placement new 配合使用
  3. 需要兼容旧代码,且确认构造函数不会分配资源
cpp 复制代码
// 场景1:分配原始内存
char* buffer = new (std::nothrow) char[1024];
if (buffer) {
    // 使用 buffer...
    delete[] buffer;
}

// 场景2:与 placement new 配合
void* rawMem = std::malloc(sizeof(Widget));
if (rawMem) {
    Widget* pw = new (rawMem) Widget;  // placement new
    pw->~Widget();                     // 手动析构
    std::free(rawMem);
}

七、总结

要点 说明
new-handler 内存分配失败、抛出异常前被调用的用户自定义函数
设置方式 std::set_new_handler(func_ptr)
设计策略 释放内存、切换 handler、卸除 handler、抛异常、终止程序
Class 专属 通过自定义 operator new + RAII 辅助类实现
代码复用 使用 CRTP Mixin 基类模板复用 new-handler 逻辑
nothrow new 仅保证内存分配不抛异常,构造函数仍可能抛异常

💡 最佳实践

  1. 为应用程序设置一个合理的全局 new-handler
  2. 对于资源敏感类,考虑实现专属的 new-handler
  3. 使用 RAII 确保全局 new-handler 总能被正确恢复
  4. 谨慎使用 nothrow new,理解其局限性

掌握 new-handler 机制,你就能在内存分配失败时优雅地处理异常,而不是让程序莫名其妙地崩溃。这是编写健壮 C++ 程序的重要一环!


参考资料:《Effective C++》第三版,Scott Meyers 著