70 / 80 · C++17 · 约 10 分钟
Allocator 与 pmr:资源选择和生命周期
分配器把容器的存储获取策略与元素管理分开。C++17 pmr 允许运行时选择内存资源,但资源并不由容器共享拥有;缓冲区、资源和使用它们的对象必须按正确顺序生存与销毁。
分配存储与构造对象是两件事
容器通过分配器及 allocator_traits 获取满足大小、对齐要求的存储,再在其中构造和销毁元素。换分配器不会把 vector 的元素变成节点,也不会改变其迭代器失效规则。自定义分配策略适合已确认分配开销或生命周期模式的场景,不应作为尚未测量的通用优化。
传统分配器类型是容器类型的一部分;C++17 的 pmr::polymorphic_allocator<T> 通过运行时 memory_resource 接口选择策略,让同一种 pmr 容器类型可以使用不同资源。资源指针不表示共享所有权,容器不会自动延长资源的生命周期。
单调资源适合整体释放的一批对象
monotonic_buffer_resource 从已有缓冲区分配,空间不足时向上游资源申请更多存储。单次 deallocate 不回收空间,等 release 或资源销毁时整体释放上游内存,因此很适合一次请求、一次解析或一轮构建产生的一批对象,不适合长期不断删除又插入且期待即时复用的容器。
预留 vector 的最终规模在此尤其重要:扩容留下的旧存储不会立即复用,累计消耗可能高于最终容量。增长倍率和内存布局是实现细节。若绝不允许回退到堆,可以显式选择 null_memory_resource 作上游并处理分配失败,不能仅凭提供了一块栈缓冲区就宣称程序无堆分配。
销毁顺序与分配器传播
先定义缓冲区,再定义资源,最后构造容器,使逆序析构时容器先销毁。release 不调用元素析构函数;即使 clear 已删光元素,vector 仍可能保留资源提供的容量,因此安全做法是销毁所有使用者后再 release。不能返回一个引用局部资源的 pmr 容器或字符串视图。
示例使用 pmr::vector<pmr::string>,使容器存储与字符串内部存储都使用该资源;普通 std::string 不会因此自动改用它。离开作用域前复制到普通 string,得到独立结果。pmr 分配器不在容器赋值和 swap 时传播,资源不等的 swap 不合法;跨资源移动赋值也可能逐元素移动,而非直接接管缓冲区。
容易答错的地方
- 资源释放不等于对象析构;先 release 再让仍持有该存储的容器继续操作或析构会违反生命周期和资源使用要求。
- pmr 容器类型相同不代表分配器相等,也不保证移动零成本;不同资源实例之间的 swap 必须检查资源等价性。
运行一个例子
最低标准 C++17 · 完整程序 · 下载 .cpp
#include <array>
#include <cassert>
#include <cstddef>
#include <iostream>
#include <memory_resource>
#include <string>
#include <vector>
std::string build_name() {
alignas(std::max_align_t) std::array<std::byte, 1024> buffer{};
std::pmr::monotonic_buffer_resource arena(buffer.data(), buffer.size());
std::string result;
{
std::pmr::vector<std::pmr::string> names{&arena};
names.reserve(2);
names.emplace_back("alpha");
names.emplace_back("beta");
assert(names[0] == "alpha" && names[1] == "beta");
assert(names[0].get_allocator().resource() == &arena);
result.assign(names[0].data(), names[0].size());
result += ':';
result.append(names[1].data(), names[1].size());
}
arena.release();
return result;
}
int main() {
std::string name = build_name();
assert(name == "alpha:beta");
std::cout << name << '\n';
}
在本地编译
g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread stl-allocators.cpp -o example && ./example预期结果
alpha:beta
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
函数内部创建 monotonic_buffer_resource,并返回绑定该资源的 pmr::vector<int>。即使返回值发生移动或复制消除,为什么仍不安全?给出两个可行设计。
查看参考答案
移动或复制消除只改变容器对象的构造方式,不会延长局部资源寿命;返回后资源已销毁,元素存储或其分配器引用不再可用。可以由调用者创建资源并传入,确保它覆盖返回容器整个生命周期;或者在函数结束前把结果复制到使用独立存储的普通 vector<int> 再返回。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。