Skip to main content

Chapter 7: Allocator System

Allocator 解决的问题是:容器要管理元素序列,却不应把“内存从哪里来、如何批量取得、何时归还、是否使用池化策略”写死在容器逻辑里。std::vector<T>std::list<T>std::map<K, V> 都要创建和销毁元素,但它们的结构差异很大;allocator 给这些容器提供统一的内存策略入口,让容器把结构不变量、元素生命周期和异常安全路径稳定地组织起来。

本章的贯穿材料是一段“带状态 allocator 的 std::vector<Trace> 扩容”。它足够小,却能暴露 allocator 系统的主干:先取得未初始化原始存储,再在这块存储上构造元素;扩容时把旧元素迁移到新存储;失败时销毁已经构造的新元素并归还新存储;提交成功后销毁旧元素并归还旧存储。读完本章后,应能追踪容器中一段元素从“没有对象的字节空间”进入“有效对象”,再回到“可释放存储”的完整路径。

Allocator 的工程判断顺序可以固定为四步。第一步看容器要分配的是元素数组还是内部节点;第二步看 allocator 接口负责原始存储,还是对象构造;第三步看 allocator_traits 如何补齐接口、改绑节点类型并决定复制、移动、交换时 allocator 是否跟随容器状态传播;第四步看具体策略是默认全局分配、池化分配,还是 std::pmr 的运行时资源分配。

本章只讨论 C++ 标准语义、常见实现形状和教学简化代码。真实 libstdc++、libc++、MSVC STL 会有自己的内部类型、异常保护对象和优化分支;正文不会声称已经验证某个本地实现的具体文件或行号。需要回到标准库接口时,可以对照 std::allocator_traitsuninitialized memory algorithmsstd::pmr::polymorphic_allocator 的接口说明。

7.1 allocator 作为容器内存策略

Allocator 是容器模板参数中的内存策略对象。以 std::vector<T, Alloc> 为例,T 决定元素类型,Alloc 决定容器如何为 T 取得和归还原始存储。容器自身仍然负责 sizecapacity、迭代器边界、元素顺序和异常安全提交点;allocator 只进入内存取得、归还和对象构造相关的路径。

先用一个短代码现象固定观察对象。这个 allocator 只记录分配次数,真实内存仍交给全局 operator newoperator delete。它的目的在于让 vector 的扩容路径变得可观察:每次容量不足时,容器会通过 allocator 获取一段更大的原始存储。

#include <cstddef>
#include <limits>
#include <memory>
#include <new>
#include <vector>

struct Trace {
int value{};

explicit Trace(int v) : value(v) {}
Trace(const Trace&) = default;
Trace(Trace&&) noexcept = default;
Trace& operator=(const Trace&) = default;
Trace& operator=(Trace&&) noexcept = default;
};

template<class T>
struct CountingAllocator {
using value_type = T;

int* allocation_count{};

CountingAllocator() noexcept = default;
explicit CountingAllocator(int& counter) noexcept : allocation_count(&counter) {}

template<class U>
CountingAllocator(const CountingAllocator<U>& other) noexcept
: allocation_count(other.allocation_count) {}

T* allocate(std::size_t n) {
if (n > std::numeric_limits<std::size_t>::max() / sizeof(T)) {
throw std::bad_alloc{};
}
if (allocation_count) {
++*allocation_count;
}
return static_cast<T*>(::operator new(n * sizeof(T)));
}

void deallocate(T* p, std::size_t) noexcept {
::operator delete(p);
}

template<class U>
bool operator==(const CountingAllocator<U>& rhs) const noexcept {
return allocation_count == rhs.allocation_count;
}

template<class U>
friend struct CountingAllocator;
};

int main() {
int allocations = 0;
std::vector<Trace, CountingAllocator<Trace>> values{CountingAllocator<Trace>{allocations}};

values.emplace_back(1);
values.emplace_back(2);
values.emplace_back(3);
}

这段代码里的 CountingAllocator<Trace>vector 类型的一部分。std::vector<Trace>std::vector<Trace, CountingAllocator<Trace>> 是不同类型,因为 allocator 影响容器对象如何管理存储。这个事实会影响函数参数、成员变量类型和 ABI 边界;工程中引入自定义 allocator 时,需要把它看作容器类型设计的一部分。

Allocator 的第一层作用是隔离内存来源。默认 std::allocator<T> 通常把请求转到全局分配函数;自定义 allocator 可以从预分配缓冲区、共享内存、arena、对象池或调试分配器中取得存储。容器代码仍然写成“需要 nT 的存储”,内存策略把这个请求翻译成具体来源。

Allocator 的第二层作用是把容器结构和元素生命周期拆开。vector 需要连续存储,所以它一次申请一段能容纳多个 T 的空间;listmap 常见实现使用节点,所以它们会为内部节点分配存储。两类容器都可以通过 allocator 系统表达请求,但请求的对象类型、分配频率、局部性和失败清理路径不同。

Allocator 的第三层作用是为源码阅读提供切入点。阅读容器实现时,一旦看到 allocator_typealloc_traitsallocateconstructdestroydeallocaterebind_alloc 这一组名字,就应把注意力放到三个问题上:当前要创建的是用户元素还是内部节点;当前处在原始存储阶段还是有效对象阶段;异常发生后由谁销毁已构造对象并归还存储。

7.2 allocate、deallocate、construct 与 destroy

Allocator 路径中最容易混淆的是“取得存储”和“创建对象”。allocate(n) 取得的是足够容纳 nT 的未初始化存储,返回值可以被当作 T* 位置使用,但该位置上的 T 对象生命周期尚未开始。对象生命周期从构造动作开始,到析构动作结束;存储归还发生在对象销毁之后。

可以把一个元素位置的状态按顺序看成四段:无存储、原始存储、有效对象、原始存储。allocate 把状态从无存储推进到原始存储;construct 在原始存储上开始对象生命周期;destroy 结束对象生命周期;deallocate 归还原始存储。容器的异常安全代码就在这些状态之间建立清理规则。

下面的示例直接使用 std::allocator_traits 调用 allocator。它展示了容器内部常见的动作顺序。代码没有手写 placement new,因为标准容器通常通过 traits 统一访问 allocator。

#include <memory>
#include <string>

void build_one_string() {
using Alloc = std::allocator<std::string>;
using Traits = std::allocator_traits<Alloc>;

Alloc alloc;
std::string* p = Traits::allocate(alloc, 1);

try {
Traits::construct(alloc, p, "allocator path");
Traits::destroy(alloc, p);
Traits::deallocate(alloc, p, 1);
} catch (...) {
Traits::deallocate(alloc, p, 1);
throw;
}
}

这个示例的关键点在异常分支。allocate 成功后,如果 construct 抛出异常,p 指向的存储已经取得,但 std::string 对象尚未成功建立。清理动作只需要 deallocate。如果构造已经成功,后续离开作用域前必须先 destroy,再 deallocate。把这两个阶段混在一起,会导致对未开始生命周期的对象调用析构,或者释放仍含有有效对象的存储。

constructdestroy 的接口形态经历过标准演进。C++11 引入 std::allocator_traits,标准容器通过 traits 调用 allocator 相关操作;C++17 起 allocator 成员 constructdestroy 在若干上下文中逐步退场,C++20 以后更推荐把对象构造表达为 std::construct_at 这一类显式生命周期工具。阅读现代标准库源码时,看到 allocator_traits<Alloc>::construct 仍可按“在已分配存储上开始对象生命周期”理解。

new T(args...) 表达式会把分配和构造组合成一个动作:先通过分配函数取得存储,再构造对象。Allocator 路径把组合动作拆开,是因为容器需要批量管理一段存储。vector 扩容时先申请能容纳多个元素的新块,再逐个移动或拷贝旧元素;其中任意一个元素构造失败,都要销毁已经构造成功的前缀,并归还整块新存储。这种批量场景无法用单个 new T 表达清楚。

deallocate(p, n)n 也有工程意义。allocator 可能按块大小归还存储,或者在调试分配器中校验申请和释放的元素数量是否匹配。容器必须保存足够的信息,保证释放时传回的指针和数量与当初的分配请求匹配。对 vector 来说,这通常来自 capacity;对节点式容器来说,节点 allocator 通常一次释放一个节点。

7.3 allocator_traits、rebind 与 propagation

std::allocator_traits<Alloc> 是标准库访问 allocator 的统一入口。容器源码若直接写 Alloc::pointerAlloc::constructAlloc::rebind,就会把自己绑定到某一种 allocator 接口形状;traits 把这些差异收束起来,提供默认类型、默认构造动作、默认销毁动作和传播策略查询。

Traits 的第一个作用是补齐类型。allocator 可以提供 pointerconst_pointersize_typedifference_type 等成员;缺席时,allocator_traits 会按规则推导出常见类型。对多数普通 allocator,pointer 就是 T*。对共享内存或特殊地址空间 allocator,pointer 可能是一个指针状对象。容器通过 traits 读取这些类型,才能在接口层保留 allocator 的扩展空间。

Traits 的第二个作用是改绑类型,也就是 rebind。容器模板参数里的 allocator 常常写成 Alloc<T>,但节点式容器内部真正分配的对象可能是 list_node<T>tree_node<Value>。这时容器需要从“分配用户元素的 allocator”得到“分配内部节点的 allocator”。allocator_traits<Alloc>::rebind_alloc<Node> 就承担这个转换。

下面的简化代码展示了节点式容器会怎样使用 rebind。它不是某个标准库实现的源码,只表达阅读真实实现时应识别的形状。

#include <memory>
#include <utility>

template<class T>
struct ListNode {
ListNode* prev{};
ListNode* next{};
T value;

template<class... Args>
ListNode(ListNode* p, ListNode* n, Args&&... args)
: prev(p), next(n), value(std::forward<Args>(args)...) {}
};

template<class T, class Alloc = std::allocator<T>>
class MiniListStorage {
using Node = ListNode<T>;
using NodeAlloc = typename std::allocator_traits<Alloc>::template rebind_alloc<Node>;
using NodeTraits = std::allocator_traits<NodeAlloc>;

NodeAlloc node_alloc_;

public:
template<class... Args>
Node* create_node(Args&&... args) {
Node* node = NodeTraits::allocate(node_alloc_, 1);
try {
NodeTraits::construct(node_alloc_, node, nullptr, nullptr, std::forward<Args>(args)...);
return node;
} catch (...) {
NodeTraits::deallocate(node_alloc_, node, 1);
throw;
}
}

void destroy_node(Node* node) noexcept {
NodeTraits::destroy(node_alloc_, node);
NodeTraits::deallocate(node_alloc_, node, 1);
}
};

这段代码把 Alloc 改绑成 NodeAlloc,原因是 list 的分配单位通常是节点。节点中包含链接指针和 T 值;用户看到的是 T 序列,容器维护的是节点网络。源码中一旦出现 rebind_alloc<__node>__node_alloc_traits 这类名字,就应判断当前 allocator 操作落在内部节点层级,而非直接落在用户元素层级。

Traits 的第三个作用是描述 allocator 状态在容器复制、移动和交换时如何处理。三个传播标志分别对应 copy assignment、move assignment 和 swap:propagate_on_container_copy_assignmentpropagate_on_container_move_assignmentpropagate_on_container_swap。这些标志影响容器能否直接接管另一侧存储,还是需要逐个元素重新构造。

is_always_equal 也会进入这个判断。若所有 allocator 实例都能彼此释放对方分配的存储,容器移动时可以把缓冲区指针直接转移到目标对象。若 allocator 带有资源身份,例如指向不同内存池的指针,那么两个 allocator 比较结果会影响移动赋值的成本和合法释放路径。状态 allocator 让“移动容器”从简单指针交换,变成必须检查资源归属的操作。

容器源码中的 propagation 逻辑可以用一个稳定顺序阅读:先看目标容器和源容器的 allocator 是否会传播;再看 allocator 是否相等或总是等价;再看容器能否接管源存储;最后看失败路径是否需要保留旧状态。这个顺序能解释为什么同样是 vector 移动赋值,有些情况下接近常数成本,有些情况下需要按元素移动。

7.4 raw memory algorithm 与 uninitialized 操作

Raw memory algorithm 处理的是“目标区域有存储,但对象尚未构造”的场景。std::uninitialized_copystd::uninitialized_movestd::uninitialized_fill 会在未初始化目标区域中逐个构造对象;std::destroystd::destroy_n 会结束一段已构造对象的生命周期。它们服务的主场景就是容器批量创建和批量清理元素。

vector 扩容是理解这些算法的最佳材料。扩容前,旧缓冲区 [old_begin, old_end) 中有有效对象,[old_end, old_cap) 是已取得但尚未构造元素的存储。扩容时,容器申请新缓冲区 [new_begin, new_cap),然后把旧元素移动或拷贝到新缓冲区的前缀。新缓冲区在迁移完成前只是一段原始存储,迁移过程会逐个开始元素生命周期。

这个流程包含状态迁移和异常分支,适合用 Mermaid 表达主路径。图中“新前缀”指新缓冲区中已经构造成功的一段对象。

图中的核心边界是提交点。提交点之前,旧缓冲区仍然代表容器的有效状态;新缓冲区只是候选状态。若迁移失败,容器销毁新缓冲区中已经构造出来的元素并释放新存储,旧缓冲区继续承担容器状态。若迁移成功,容器再销毁旧元素、释放旧存储,并把内部指针切到新缓冲区。

下面的简化代码展示扩容中的 raw memory 操作。它省略了容量增长策略和 allocator propagation,只保留状态路径。

#include <memory>
#include <utility>

template<class T, class Alloc>
T* relocate_to_new_storage(Alloc& alloc, T* first, T* last, std::size_t new_capacity) {
using Traits = std::allocator_traits<Alloc>;

T* new_first = Traits::allocate(alloc, new_capacity);
T* new_current = new_first;

try {
for (T* current = first; current != last; ++current, ++new_current) {
Traits::construct(alloc, new_current, std::move_if_noexcept(*current));
}
return new_first;
} catch (...) {
for (T* p = new_first; p != new_current; ++p) {
Traits::destroy(alloc, p);
}
Traits::deallocate(alloc, new_first, new_capacity);
throw;
}
}

这段代码中,new_current 是异常安全的关键变量。它把“已经成功构造的前缀”记录下来。异常发生时,清理循环只销毁 [new_first, new_current),因为这个范围内对象生命周期已经开始;[new_current, new_first + new_capacity) 仍然只是原始存储,销毁它们没有意义。

uninitialized_copyuninitialized_move 可以把上面的循环封装成算法。它们的价值在于让“把一个输入区间构造到未初始化目标区间”成为独立操作,并把部分构造失败时的清理规则纳入算法语义。标准说明中对这些算法的描述也强调:目标区域是未初始化内存,异常发生时已构造对象会被销毁。

Raw memory algorithm 和普通算法的边界在目标区域状态。std::copy 的目标位置已经有有效对象,因此它执行赋值;std::uninitialized_copy 的目标位置只有存储,因此它执行构造。把这两个算法混用,会破坏对象生命周期:在未构造区域赋值没有有效目标对象,在已有对象区域再次构造会覆盖有效对象状态。

7.5 pool allocator、polymorphic allocator 与 std::pmr

Pool allocator 的目标是改变分配成本模型。默认分配器通常把每次请求交给通用堆分配路径;节点式容器在频繁插入、删除小对象时,会产生大量小块分配。对象池把一批同尺寸或近似尺寸的块集中管理,单次节点分配可以变成从空闲链表取出一个块,释放可以变成把块放回空闲链表。

池化策略最适合“生命周期集中、尺寸稳定、分配频率高”的场景。listmapunordered_map 的节点分配都可能从池化中获得收益,因为它们常见实现会为每个节点单独分配存储。vector 的主要成本常常来自连续大块重新分配和元素迁移,池化对它的收益要结合容量增长、元素移动成本和内存局部性一起判断。

Pool allocator 也会改变失败和所有权边界。池对象需要有明确生命周期:容器元素销毁前,池资源必须仍然有效;容器把存储归还 allocator 时,allocator 必须能识别这块存储属于哪个池。带状态 allocator 的比较结果、传播标志和 is_always_equal 都服务于这个资源归属问题。

C++17 引入 std::pmr,把 allocator 的策略变化从“容器静态类型变化”转成“运行时 memory_resource 变化”。std::pmr::polymorphic_allocator<T> 是 allocator,内部持有 std::pmr::memory_resource*;不同容器对象可以拥有相同的静态 allocator 类型,却把请求转发给不同资源,例如单调缓冲资源、同步池资源或非同步池资源。

下面的代码展示 std::pmr 的典型形状。容器类型使用 std::pmr::vector,实际内存来源由构造时传入的 resource 决定。

#include <array>
#include <memory_resource>
#include <string>
#include <vector>

void build_names() {
std::array<std::byte, 4096> buffer{};
std::pmr::monotonic_buffer_resource arena{buffer.data(), buffer.size()};

std::pmr::vector<std::pmr::string> names{&arena};
names.emplace_back("red");
names.emplace_back("black");
names.emplace_back("tree");
}

std::pmr::vector<std::pmr::string> 这个例子有两层分配。第一层是 vector 自己的元素数组,第二层是每个 pmr::string 可能需要的字符缓冲区。polymorphic_allocator 的 uses-allocator construction 会把同一个 resource 传进嵌套对象,使 vector 存储和字符串存储都来自 arena。这正是 std::pmr 适合表达 arena 场景的原因。

monotonic_buffer_resource 的常见使用方式是批量申请、批量释放。它按增长块提供存储,单个对象的释放通常不会把内存立刻还给上游资源;资源销毁或重置时统一释放所管理的块。它适合请求生命周期集中结束的场景,例如一次解析任务、一次编译中间结构、一次请求处理中的临时对象图。

unsynchronized_pool_resourcesynchronized_pool_resource 面向池化分配。前者适合单线程或外部已经串行化的场景,后者带同步成本,可在多线程共享资源时使用。选择它们时应先看分配是否真的集中在小对象上,再看线程共享方式,最后看资源对象生命周期是否覆盖所有容器和嵌套对象。

std::pmr 的主要工程收益是降低模板类型扩散。自定义 MyPoolAllocator<T> 会让 std::vector<T, MyPoolAllocator<T>> 变成独立静态类型;std::pmr::vector<T> 的静态类型保持稳定,具体资源通过构造参数切换。代价是资源分配使用虚函数分派,且 resource 的生命周期必须由工程代码明确管理。

7.6 allocator 对容器实现与 mini allocator 的影响

Allocator 对不同容器的影响首先取决于分配单位。vector 分配的是连续元素存储,核心路径是容量不足时申请新块、迁移旧元素、提交三指针状态。list 分配的是节点,核心路径是构造节点、连接前后指针、失败时释放单个节点。map 常见实现分配树节点,核心路径是构造节点值、插入红黑树结构、在旋转和修复过程中保持节点地址稳定。

三类容器可以使用同一组维度比较。所有权方面,容器拥有元素生命周期,allocator 拥有存储来源策略。内存布局方面,vector 追求连续存储,节点容器追求局部插入和迭代器稳定。异常安全方面,vector 的难点在批量迁移和提交点,节点容器的难点在节点构造成功后再进入结构链接。复杂度方面,allocator 无法改变容器承诺的抽象复杂度,但可以改变常数成本、局部性、碎片和锁竞争。

Mini allocator 的实现检查点应围绕标准容器真正会调用的入口展开。最小可用形状需要 value_type、跨类型拷贝构造、allocatedeallocate 和相等比较。对类模板 template<class T> Alloc 来说,allocator_traits 通常可以推导 rebind;复杂 allocator 仍然可以显式提供 rebind、pointer 类型、传播标志和 is_always_equal

下面的版本适合作为 mini allocator 的阅读模型。它有状态,所以相等比较使用状态指针;它没有声明传播标志,因此 traits 会采用默认传播规则。真实工程中还需要更严格的测试、对齐检查、统计回收和资源生命周期设计。

#include <cstddef>
#include <limits>
#include <new>

template<class T>
class MiniAllocator {
public:
using value_type = T;

MiniAllocator() noexcept = default;
explicit MiniAllocator(int& counter) noexcept : counter_(&counter) {}

template<class U>
MiniAllocator(const MiniAllocator<U>& other) noexcept : counter_(other.counter_) {}

T* allocate(std::size_t n) {
if (n > std::numeric_limits<std::size_t>::max() / sizeof(T)) {
throw std::bad_alloc{};
}
if (counter_) {
++*counter_;
}
return static_cast<T*>(::operator new(n * sizeof(T)));
}

void deallocate(T* p, std::size_t) noexcept {
::operator delete(p);
}

template<class U>
bool operator==(const MiniAllocator<U>& rhs) const noexcept {
return counter_ == rhs.counter_;
}

private:
template<class U>
friend class MiniAllocator;

int* counter_{};
};

用这个 allocator 读 vector,检查点是:构造容器时 allocator 状态如何进入容器对象;扩容时 allocaten 对应新容量;元素构造通过 allocator_traits::construct 发生;异常时已经构造的新前缀如何销毁;提交成功后旧元素和旧存储如何释放。这个顺序能定位大多数 vector allocator 路径。

用这个 allocator 读 listmap,检查点要转到节点层级。源码中用户传入的是 Alloc<T>,内部使用的可能是 rebind_alloc<Node>。节点分配成功后,容器需要构造节点对象或节点内的 value,然后把节点接入链表或树结构。异常发生在构造阶段时,只释放未接入结构的节点;异常发生在比较器或插入定位阶段时,容器状态仍由旧结构代表。

Allocator 的最终判断顺序可以沉淀为一条源码阅读路径:先确定分配对象是元素还是节点;再区分当前动作是取得存储、构造对象、销毁对象还是归还存储;然后查 allocator_traits 是否引入 rebind、传播标志和等价性判断;最后把这些动作放回容器结构中,看它们如何影响迭代器稳定性、异常安全提交点、复杂度常数和内存局部性。

最小自检任务

阅读下面的代码片段,只根据本章内容判断三个问题:storage 取得后是否已经存在两个 Widget 对象;第二个对象构造失败时需要销毁哪些对象;最后释放存储时为什么要传回 2

#include <memory>
#include <stdexcept>

struct Widget {
int value{};

explicit Widget(int v) : value(v) {
if (v == 2) {
throw std::runtime_error{"bad widget"};
}
}
};

void build_two() {
using Alloc = std::allocator<Widget>;
using Traits = std::allocator_traits<Alloc>;

Alloc alloc;
Widget* storage = Traits::allocate(alloc, 2);
Widget* current = storage;

try {
Traits::construct(alloc, current, 1);
++current;
Traits::construct(alloc, current, 2);
++current;
} catch (...) {
for (Widget* p = storage; p != current; ++p) {
Traits::destroy(alloc, p);
}
Traits::deallocate(alloc, storage, 2);
throw;
}

for (Widget* p = storage; p != current; ++p) {
Traits::destroy(alloc, p);
}
Traits::deallocate(alloc, storage, 2);
}

答案要点

Traits::allocate(alloc, 2) 只取得能容纳两个 Widget 的原始存储,两个对象的生命周期尚未开始。第一个 construct 成功后,storage 指向的位置才有一个有效 Widgetcurrent 增加后指向第二个位置。第二个 constructvalue == 2 抛出异常时,第二个对象没有成功建立,清理循环只销毁 [storage, current),也就是第一个已经构造成功的对象。deallocate(alloc, storage, 2) 传回 2,因为 allocator 当初收到的分配请求就是两个 Widget 的存储,释放时需要用同一块指针和匹配的数量归还资源。

这个判断对应容器扩容的核心规则:allocator 先管理原始存储,构造动作逐个开始对象生命周期,异常清理只覆盖已经成功构造的前缀,释放存储时使用当初的容量信息。若把 allocate 理解成创建对象,就会错误地销毁尚未开始生命周期的位置;若忽略容量参数,就无法解释 allocator 如何校验或回收对应大小的块。

本章知识点总结

  • 策略入口:allocator 是容器模板参数中的内存策略对象,容器仍负责结构状态、元素生命周期和异常安全提交点。
  • 原始存储allocate 取得未初始化存储,返回位置尚未包含有效 T 对象。
  • 生命周期construct 开始对象生命周期,destroy 结束对象生命周期,二者必须和存储状态匹配。
  • 释放匹配deallocate 应使用 allocator 曾经返回的指针和匹配的元素数量归还存储。
  • traits 入口allocator_traits 统一访问 allocator 类型、分配函数、构造函数、销毁函数和复制构造时的选择规则。
  • rebind 作用:节点式容器通过 rebind 从元素 allocator 得到内部节点 allocator,从而为链表节点或树节点分配存储。
  • 传播规则:propagation 标志和 allocator 等价性决定容器复制、移动、交换时能否接管另一侧存储。
  • 未初始化算法uninitialized_copyuninitialized_moveuninitialized_fill 在原始存储上批量构造对象。
  • 异常前缀:批量构造失败时只销毁已经构造成功的前缀,再释放候选存储。
  • vector 扩容:扩容先申请新存储并迁移元素,成功后提交新指针,失败时保留旧缓冲区状态。
  • 池化分配:pool allocator 通过集中管理小块存储降低频繁节点分配的常数成本和碎片压力。
  • pmr 边界std::pmr 把内存策略放入运行时 memory_resource,容器静态类型保持稳定,资源生命周期需要工程代码保证。
  • 容器差异vector 的 allocator 难点在连续块迁移,listmap 的 allocator 难点在节点分配、构造和结构接入。
  • 阅读顺序:先看分配对象,再看存储和生命周期阶段,再看 traits 规则,最后回到容器结构和异常安全路径。