std::vector 自定义分配器
引问:vector 的第二个模板是 allocator,它会调用一个什么bind 之类的东西,是个什么概念?
在 C++ 标准库里,std::vector 模板的第二个参数确实是分配器(Allocator),它负责管理容器内部元素内存的分配和释放。为了让同一个分配器类型能够为不同类型的对象分配内存(比如 vector 内部可能要给 T 以外的临时对象分配空间),标准定义了一个“重绑定”(rebind)机制。
Allocator::rebind
-
概念
Allocator 是一个模板类,它只能直接分配类型 T 的内存。但在操作容器内部结构时(比如分配节点、维护内存池、构造/销毁元素),可能需要分配其他类型的内存(例如内部节点、控制块、对齐缓冲区等)。rebind 就是告诉编译器 “嘿,如果我有一个为 T 服务的分配器类型,怎么把它‘转换’成一个为 U 服务的分配器类型?” -
典型定义
在一个自定义分配器里,你可能会看到类似:
template<typename U>
struct rebind {using other = MyAllocator<U>;
};
这就声明:
给定 MyAllocator,如果想要一个 MyAllocator<U>,就通过 MyAllocator<T>::rebind<U>::other 拿到。
- 标准适配:allocator_traits
从 C++11 开始,标准引入了 std::allocator_traits,来统一分配器的接口。allocator_traits 会检查你的分配器类型是否定义了 rebind 嵌套模板,如果没有,则提供一个默认实现:
template<typename Alloc, typename U>
using rebinder = typename Alloc::template rebind<U>::other;
当容器需要为其他类型(如内部节点类型)分配内存时,就使用:
using NodeAlloc = typename std::allocator_traits<Allocator>::template rebind_alloc<NodeType>;
这里的 rebind_alloc 就是通过 rebind::other 或内部默认实现得到的。
在 std::vector 中的作用
• 分配元素数组
最直接的用途是为 T[] 分配内存,构造和析构 T。这是最常见的:vector<T, Alloc> 内部会用到 Alloc::allocate(n) 分配 n * sizeof(T) 字节。
• 分配内部控制结构
比如在某些实现里,vector 可能会分配额外的控制块或对齐缓冲区(通常不是用在 vector 上,但对 list、map 之类的节点容器就很常见)。
• 容器之间的拷贝/移动
当你从一个使用分配器 A1 的容器,拷贝到另一个使用分配器 A2 的容器时,如果 A1 和 A2 不是同类型,需要把 A1 重绑定到目标元素类型以便做内存复制。
小结
• rebind:分配器模板里的一个内部模板,用来“重绑定”到其他类型。
• allocator_traits:C++11+ 统一接口,检测或提供默认的 rebind 实现。
• 用途:让同一个分配器类型能在容器内部为多种类型分配内存,不仅仅是容器元素本身。
这样,无论你在容器内部需要分配的是 T、Node、还是一些内部对齐缓冲,这个机制都能让分配器“通吃”各种类型。
下面是一个简单的自定义分配器 MyAllocator,它满足 C++11 的分配器要求,并且实现了 rebind 功能。你可以用它来替换 std::allocator,例如:std::vector<int, MyAllocator>。
#include <cstddef> // for std::size_t, std::ptrdiff_t
#include <memory> // for std::allocator_traits
#include <new> // for ::operator new, ::operator deletetemplate <typename T>
class MyAllocator {
public:using value_type = T;using pointer = T*;using const_pointer = const T*;using reference = T&;using const_reference = const T&;using size_type = std::size_t;using difference_type = std::ptrdiff_t;// 重绑定模板,用于为其他类型 U 分配内存template <typename U>struct rebind {using other = MyAllocator<U>;};MyAllocator() noexcept = default;// 拷贝构造允许从不同类型的 MyAllocator 复制template <typename U>MyAllocator(const MyAllocator<U>&) noexcept {}~MyAllocator() = default;// 分配 n 个 T 对象的原始内存pointer allocate(size_type n) {if (n == 0) return nullptr;// 简单地使用全局 operator newvoid* ptr = ::operator new(n * sizeof(T));return static_cast<pointer>(ptr);}// 释放从 allocate 分配的内存void deallocate(pointer p, size_type) noexcept {::operator delete(p);}// 在已分配的内存上原地构造对象template <typename U, typename... Args>void construct(U* p, Args&&... args) {::new ((void*)p) U(std::forward<Args>(args)...);}// 显式调用析构template <typename U>void destroy(U* p) {p->~U();}// 最大可分配对象数量(理论上)size_type max_size() const noexcept {return size_type(-1) / sizeof(T);}
};// 比较运算符:不同类型的 MyAllocator 总是相等的
template <typename T, typename U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) noexcept {return true;
}template <typename T, typename U>
bool operator!=(const MyAllocator<T>& a, const MyAllocator<U>& b) noexcept {return !(a == b);
}
说明:
1. value_type 等类型定义
标准要求分配器提供这些类型别名,以便 allocator_traits 能正确处理。
2. rebind 嵌套模板
用来将 MyAllocator<T> 变换为 MyAllocator<U>,容器内部需要为其他类型(如节点或缓冲区)分配时会用到。
3. allocate / deallocate
使用全局的 ::operator new 和 ::operator delete。在真实场景,你可以替换成自定义的内存池或对齐分配逻辑。
4. construct / destroy
在 C++17 及以前需要自定义,但从 C++20 起 allocator_traits 会使用 std::allocator_traits::construct,你可以省略这两个方法,让 allocator_traits 使用完美转发到 ::new。
5. 比较运算符
分配器之间相等意味着它们可以互换内存资源。在这里我们简单地认为所有 MyAllocator 实例都相等。
你可以像这样使用它:
#include <vector>
int main() {// 使用 MyAllocator 分配 vector 的内存std::vector<int, MyAllocator<int>> v;v.push_back(42);v.push_back(7);return 0;
}
这个示例展示了如何编写一个符合标准分配器接口、支持重绑定(rebind)的最简实现。
在 std::vector<T, Alloc> 的实现中,会通过 allocator_traits<Alloc>::rebind_alloc<U>(或直接用 Alloc::rebind<U>::other)来为不同用途分配内存。下面给出几个典型场景和简化示例,说明它们如何“引用”到 other:
⸻
- 分配元素数组
当你做 vector.reserve(n) 或第一次插入元素时,vector 需要为 n 个元素分配一片连续内存。它会这样写:
using AllocTraits = std::allocator_traits<Alloc>;
using TAlloc = Alloc; // 原始的元素分配器
using UAlloc = AllocTraits:: // 重绑定到 Ttemplate rebind_alloc<T>; // … 在成员函数里 …
UAlloc alloc = get_allocator(); // 拿到一个 T 分配器实例
pointer data = AllocTraits::allocate( // 调用 allocate(n)alloc, static_cast<size_type>(n)
);
在 C++11 以前,等价于:
using UAlloc = typename Alloc::template rebind<T>::other;
UAlloc alloc = static_cast<const UAlloc&>(get_allocator());
pointer data = alloc.allocate(n);
这里的 rebind::other 就是 MyAllocator 或 std::allocator。
- 重新分配(grow)时的临时缓冲
当 vector 扩容(从旧容量 cap 增长到新容量 new_cap)时,会:
// 1. 先通过重绑定分配一块更大的内存:
UAlloc tmp_alloc = get_allocator();
T* new_buf = tmp_alloc.allocate(new_cap);
// 2. 将旧元素原地拷贝或移动到新缓冲区:
for (size_type i = 0; i < size(); ++i) {AllocTraits::construct(tmp_alloc, new_buf + i, std::move_if_noexcept(data_[i]));
}
// 3. 销毁、释放旧内存,切换指针:
for (size_type i = 0; i < size(); ++i)AllocTraits::destroy(alloc, data_[i]);
alloc.deallocate(data_, cap);
data_ = new_buf;
cap = new_cap;
⸻
- 拷贝或移动构造另一个 vector
当你做 vector<int, MyAlloc> v2(v1); 时,如果 MyAlloc 支持跨类型拷贝:
template <class Alloc2>
vector(const vector<T,Alloc2>& other,const Alloc& alloc /* = Alloc()*/): alloc_(alloc), size_(other.size()), cap_(other.size())
{using UAlloc2 = typename Alloc2::template rebind<T>::other;UAlloc2 src_alloc(other.get_allocator());data_ = alloc_traits::allocate(alloc_, cap_);for (size_type i = 0; i < size_; ++i) {alloc_traits::construct(alloc_, data_ + i,other.data_[i]);}
}
这里,Alloc2::rebind::other 用来从 other 的分配器类型里拿到一个能分配 T 的实例。
⸻
- 插入时的异常安全缓冲
对于强异常安全保证,vector 插入可能在临时缓冲区中先做完整构造,然后才切换到正式内存。依然用同一个 rebind::other:
// 简化伪码
UAlloc temp_alloc = get_allocator();
pointer temp_buf = temp_alloc.allocate(cap_ + 1);try {// 构造 [0, pos) 旧元素for (...) construct(temp_buf + i, data_[i]);// 构造新元素construct(temp_buf + idx, value);// 构造剩余旧元素for (...) construct(temp_buf + j, data_[j-1]);
}
catch(...) {// 析构已经构造的元素for(...) destroy(temp_buf + k);temp_alloc.deallocate(temp_buf, cap_ + 1);throw;
}// 到此无异常,切换到 temp_buf ...
⸻
小结
• Alloc::rebind<U>::other 或 allocator_traits<Alloc>::rebind_alloc<U>
• 场景:初次分配元素数组、扩容重分配、跨分配器拷贝构造、异常安全插入等。
• 作用都是:把一个“只能分配 T”的分配器,变成“能分配 U”的分配器,供 vector 各种内部机制使用。
通过这些例子,可以看到 other 是如何被 std::vector 在内存管理各环节中反复引用的。