在C++17标准之前,想要为容器更换内存分配行为往往要依赖模板参数,比如指定std::vector<int, MyAllocator>,这不仅让类型耦合严重,还容易引发代码膨胀。std::pmr的引入把分配策略从编译期模板参数变成了运行期可替换的对象,核心思路是用一个统一的基类memory_resource来描述内存来源,容器通过polymorphic_allocator持有指向该资源的指针,从而在运行时自由切换。

一、std::pmr的核心概念
std::pmr全称是Polymorphic Memory Resources,定义在头文件<memory_resource>中。它主要由三部分组成:抽象基类std::pmr::memory_resource、适配器std::pmr::polymorphic_allocator,以及标准提供的几种具体资源实现。memory_resource只规定了三个虚函数:do_allocate、do_deallocate和do_is_equal,任何自定义内存池或专用分配器都可以继承它来提供自己的实现。
polymorphic_allocator是一个分配器类型,但它不保存具体策略,只保存一个指向memory_resource的指针。当容器调用allocate时,它会把请求转发给当前绑定的资源。因为指针可以在构造之后通过select_on_container_copy_construction等机制更换,所以我们不必重新编译容器类型,就能在程序运行中把分配来源从默认堆改成某块预分配的缓冲区。
1.1 memory_resource的基本接口
下面是一段简化版的自定义资源示例,演示如何继承memory_resource并使用一个简单的栈式缓冲区作为后端:
#include <memory_resource>
#include <cstddef>
#include <cstring>
class StackResource : public std::pmr::memory_resource {
public:
StackResource(void* buf, std::size_t size)
: buffer_(static_cast<char*>(buf)), capacity_(size), offset_(0) {}
protected:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
// 简化实现:仅按偏移累加,不做严格对齐处理
if (offset_ + bytes > capacity_) {
return ::operator new(bytes); // 超出则退回全局堆
}
void* p = buffer_ + offset_;
offset_ += bytes;
return p;
}
void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
// 栈式资源一般不单独释放,这里仅示例
if (p < buffer_ || p >= buffer_ + capacity_) {
::operator delete(p);
}
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
private:
char* buffer_;
std::size_t capacity_;
std::size_t offset_;
};
上面的代码展示了自定义资源的最小实现。实际工程中需要更严谨的对齐计算和释放逻辑,但已经足以说明memory_resource的扩展方式。由于容器只认memory_resource指针,任何符合接口的资源都能即插即用。
二、标准内置的内存资源
C++标准提供了几个可直接使用的memory_resource实现,覆盖大多数常见需求。最常用的是std::pmr::new_delete_resource,它直接调用全局operator new和delete,等价于传统默认分配器。另一个重要的是std::pmr::null_memory_resource,任何分配请求都会抛bad_alloc,常用于检测内存泄漏或禁止动态分配的场景。
最实用的当属std::pmr::unsynchronized_pool_resource和std::pmr::synchronized_pool_resource。前者是单线程内存池,把大块内存切分为固定大小的小块,显著降低频繁分配释放的开销;后者增加了内部锁,可安全用于多线程,但性能略低。此外,std::pmr::monotonic_buffer_resource提供只分配不释放的单调缓冲区,非常适合临时对象的短期密集分配。
2.1 资源获取与全局默认资源
标准库允许通过set_default_resource更换全局默认pmr资源,之后所有使用polymorphic_allocator且未显式指定资源的容器都会使用它。下面演示如何在某段逻辑中临时切换为内存池:
#include <memory_resource>
#include <vector>
#include <iostream>
int main() {
std::pmr::unsynchronized_pool_resource pool;
std::pmr::memory_resource* old = std::pmr::set_default_resource(&pool);
// 该vector使用pool作为分配后端
std::pmr::vector<int> vec;
for (int i = 0; i < 1000; ++i) {
vec.push_back(i);
}
std::cout << "capacity: " << vec.capacity() << std::endl;
std::pmr::set_default_resource(old); // 恢复默认
return 0;
}
注意上面使用的是std::pmr::vector,它是typedef std::vector<int, std::pmr::polymorphic_allocator<int>>的别名。只要不指定分配器实例,它就会跟随默认资源。这种写法让代码保持简洁,同时获得池化分配的性能收益。
三、在运行时切换分配策略
运行时切换的本质是改变polymorphic_allocator内部指向的memory_resource。最直接的方式是在构造容器时传入不同的资源指针,或者在支持的资源上调用release来重置池状态。如果容器已经存在,C++的容器复制构造规则允许通过select_on_container_copy_construction返回带新资源的分配器,但这通常意味着创建新容器。
更灵活的做法是把资源对象放在更高作用域,在业务逻辑分支中把不同资源指针传给新容器。例如解析请求时,小请求用栈缓冲区,大请求用内存池。这样无需修改容器类型,只靠参数即可控制分配来源,避免了模板多实例化的二进制膨胀。
3.1 基于场景切换的完整示例
下面例子展示一个函数根据数据规模选择不同资源,并用同一个容器类型处理:
#include <memory_resource>
#include <vector>
#include <array>
void process(bool large) {
std::array<char, 4096> buf;
std::pmr::monotonic_buffer_resource stack_res(buf.data(), buf.size());
std::pmr::unsynchronized_pool_resource pool_res;
std::pmr::memory_resource* res = large ? static_cast<std::pmr::memory_resource*>(&pool_res)
: static_cast<std::pmr::memory_resource*>(&stack_res);
std::pmr::vector<int> data(res);
// 填充数据,分配行为随res变化
for (int i = 0; i < (large ? 100000 : 100); ++i) {
data.push_back(i);
}
// 离开作用域时资源对象析构,栈缓冲区和池自动回收
}
这个示例清楚地表明,切换策略不需要重新定义类型,只需要更换传入的resource指针。monotonic_buffer_resource在栈上分配,离开函数自动失效;pool_res则管理堆内池化块。两者生命周期都由普通对象规则控制,不容易出现悬空资源。
四、常见误区与注意事项
使用std::pmr时最易犯的错误是资源对象生命周期短于容器。如果容器仍持有指向已析构资源的指针,后续分配或析构会访问无效内存。因此务必保证resource对象的存活时间覆盖所有使用它的容器。另一个误区是认为polymorphic_allocator会自动共享状态,其实每个容器只是保存指针,多个容器绑定同一资源时才真正共享后端。
性能方面,并非所有场景都适合内存池。对于生命周期长、分配次数少的大对象,直接new_delete_resource反而更简单且碎片更少。建议先用性能剖析工具确认瓶颈,再决定是否引入pool或monotonic资源。同时多线程下应优先选用synchronized版本,或确保每个线程独享unsynchronized资源,避免数据竞争。
4.1 与旧分配器模型的对比
传统std::allocator或自定义模板分配器在类型层面固定策略,导致同一逻辑但因分配器不同而产生多份机器码。std::pmr把策略下沉为运行期对象,类型统一为polymorphic_allocator,显著减少模板膨胀。下表简要对比二者差异:
| 对比维度 | 模板分配器 | std::pmr |
|---|---|---|
| 策略绑定时机 | 编译期 | 运行期 |
| 容器类型 | 随分配器变化 | 统一polymorphic_allocator |
| 切换成本 | 需重新定义类型 | 更换resource指针 |
| 代码膨胀 | 易产生多实例 | 单一实例化 |
从表中可以看出,std::pmr在保持接口简单的条件下,解决了长期困扰C++开发者的分配器耦合问题。对于需要局部优化内存且不愿引入重型框架的项目,它是标准库给出的轻量级答案。
五、总结
std::pmr通过memory_resource抽象和polymorphic_allocator适配器,把内存分配策略变成了运行期可替换的组件。开发者可以利用标准内置的池资源、单调缓冲区或自定义资源,在不改动容器类型的前提下灵活切换分配后端。只要管理好资源生命周期、选对单线程或多线程版本,就能在高频分配场景中有效降低延迟和碎片。
掌握std::pmr并不需要重写现有代码,从热点路径切入,用pmr容器替换原有vector或string,往往就能获得可观的收益。它代表了C++标准向更务实的运行时资源管理迈进的一步,也让我们能以更小代价实践分配器感知的程序设计。
std::pmr内存分配器polymorphic_allocator修改时间:2026-08-08 12:21:44