9.4. 内存耗尽处理与new_handler机制

本书至此(包括本章)我们一直强调:当operator new()和operator new[]()分配内存失败时,通常会抛出std::bad_alloc异常。这种说法在大多数情况下成立,但有个微妙细节我们尚未讨论 —— 现在需要重点解析。

设想这样一种场景:用户代码定制了内存分配函数,从预分配的数据结构中获取内存块以获得特定性能优势。假设该数据结构初始仅分配少量内存块,当初始块耗尽时才扩展容量。此时,存在两种状态:

为实现无感知的策略切换(无需用户代码显式干预),直接抛出std::bad_alloc并不理想。虽然抛出异常会终止operator new()执行,使用端代码也可捕获异常处理,但在此场景中,更希望分配失败触发特定操作后,允许分配函数在更新状态后重试。

C++通过std::new_handler处理此类场景,该类型是函数指针void(*)()的别名。其核心机制如下:

标准库已实现这套算法,但截至目前,我们自定义的operator new()和operator new[]()重载尚未包含该逻辑。为演示如何利用std::new_handler,实现一个模拟的“两阶段分配”场景。

这个示例性实现将:

#include <new>
#include <vector>
#include <iostream>

struct X {
  // 简单示例,非线程安全
  static inline int limit = 5;

  void* operator new(std::size_t n) {
    std::cout << "X::operator new() called with "
              << limit << " blocks left\n";

    while (limit <= 0) {
      if (auto hdl = std::get_new_handler(); hdl)
        hdl();
      else
        throw std::bad_alloc{};
    }

    --limit;
    return ::operator new(n);
  }

  void operator delete(void* p) {
    std::cout << "X::operator delete()\n";
    ::operator delete(p);
  }
  // 数组版本的 new/delete 同理
};

int main() {
  std::set_new_handler([]() noexcept {
    std::cout << "allocation failure, "
                "fetching more memory\n";

    X::limit = 10;
    std::set_new_handler(nullptr); // 将 handler 重置为默认
  });

  std::vector<X*> v;
  v.reserve(100);

  try {
    for (int i = 0; i != 10; ++i)
      v.emplace_back(new X);
  } catch(...) {
    // 由于 new_handler 的存在,此异常处理块在此程序中永远不会执行
    std::cerr << "out of memory\n";
  }

  for (auto p : v) delete p;
}

X::operator new()处理失败的方式:当发现无法满足后置条件时,会获取当前std::new_handler —— 若非空则调用该handler后重试分配。

所以,handler调用时:

违反这些规则会导致无限循环(伴随灾难性后果)。示例中main()安装的handler行为如下:

lambda可以作为new_handler吗?

虽然我们将std::new_handler描述为void(*)()函数指针的别名,但在示例中却安装了一个lambda表达式。这之所以可行,是因为无状态lambda(即捕获列表为空的lambda)能隐式转换为具有相同调用签名的函数指针。这一特性在多种场景中非常实用,例如:

  • 编写需要与C代码交互的C++程序

  • 调用操作系统原生API接口

  • 适配传统回调函数机制

接下来我们将进入本章最具技术深度的部分 —— 探讨如何运用C++处理非常规内存。