C++ 命名要求: 分配器(Allocator)
封装了对象访问/寻址、分配/释放和构造/析构的策略。
每个可能需要分配或释放存储的标准库组件,从 std::string、std::vector 和除了 std::array 之外的所有容器,到 std::shared_ptr 和 std::function (C++17 之前),都是通过一个 分配器(Allocator) 来完成的:一个满足以下要求的类类型的对象。
许多分配器要求的实现是可选的,因为所有分配器感知类,包括标准库容器,都通过 std::allocator_traits 间接访问分配器,并且 std::allocator_traits 提供了这些要求的默认实现。
要求
给定
- T,一个非 const、非引用类型 (C++11 之前)cv 非限定对象类型 (C++11 起)
- A,类型 T 的分配器类型
a
,类型 A 的对象- B,对于某些 cv 非限定对象类型 U 的相应 分配器 类型(通过重新绑定 A 获得)
b
,类型 B 的对象p
,类型为 std::allocator_traits<A>::pointer 的值,通过调用 std::allocator_traits<A>::allocate() 获得cp
,类型为 std::allocator_traits<A>::const_pointer 的值,通过从p
转换获得vp
,类型为 std::allocator_traits<A>::void_pointer 的值,通过从p
转换获得cvp
,类型为 std::allocator_traits<A>::const_void_pointer 的值,通过从cp
或从vp
转换获得xp
,指向某个 cv 非限定对象类型 X 的可解引用指针r
,通过表达式*p
获得的类型 T 的左值n
,类型为 std::allocator_traits<A>::size_type 的值
内部类型
类型 ID | 别名类型 | 要求 |
---|---|---|
A::pointer | (未指定)[1] | * 满足 NullablePointer、LegacyRandomAccessIterator 和 LegacyContiguousIterator。 |
A::const_pointer | (未指定) | * 满足 NullablePointer、LegacyRandomAccessIterator 和 LegacyContiguousIterator。 * A::pointer 可以转换为 A::const_pointer。 |
A::void_pointer | (未指定) | * 满足 NullablePointer。 * A::pointer 可以转换为 A::void_pointer。 * B::void_pointer 和 A::void_pointer 是相同类型。 |
A::const_void_pointer | (未指定) | * 满足 NullablePointer。 * A::pointer、A::const_pointer 和 A::void_pointer 都可以转换为 A::const_void_pointer。 * B::const_void_pointer 和 A::const_void_pointer 是相同类型。 |
A::value_type | T | |
A::size_type | (未指定) | * 一个无符号整型。 * 可以表示 A 可以分配的最大对象的大小。 |
A::difference_type | (未指定) | * 一个有符号整型。 * 可以表示由 A 分配的任意两个对象指针之间的差值。 |
A::template rebind<U>::other [2] | B | * 对于任何 U,B::template rebind<T>::other 是 A。 |
指针操作
表达式 | 返回类型 | 要求 |
---|---|---|
*p | T& | |
*cp | const T& | *cp 和 *p 标识同一个对象。 |
p->m | (原样) | 与 (*p).m 相同,如果 (*p).m 定义良好。 |
cp->m | (原样) | 与 (*cp).m 相同,如果 (*cp).m 定义良好。 |
static_cast<A::pointer>(vp) | (原样) | static_cast<A::pointer>(vp) == p |
static_cast<A::const_pointer>(cvp) | (原样) | static_cast<A::const_pointer>(cvp) == cp |
std::pointer_traits<A::pointer>::pointer_to(r) | (原样) |
存储和生命周期操作
表达式 | 返回类型 | 要求 |
---|---|---|
a.allocate(n) | A::pointer | 分配适合类型 T[n] 的数组对象的存储并创建数组,但不构造数组元素。可能抛出异常。如果 n == 0 ,返回值未指定。 |
a.allocate(n, cvp) | A::pointer | 与 a.allocate(n) 相同,但可能以未指定的方式使用 cvp (nullptr 或从 a.allocate() 获得的指针) 来帮助局部性。 |
a.allocate_at_least(n) (C++23 起) | std::allocation_result <A::pointer> | 分配适合类型 T[cnt] 的数组对象的存储并创建数组,但不构造数组元素,然后返回 {p, cnt} ,其中 p 指向存储,cnt 不小于 n 。可能抛出异常。 |
a.deallocate(p, n) | (未使用) | 解除分配 p 所指向的存储,p 必须是先前调用 allocate 或 allocate_at_least (C++23 起) 返回的值,且未被中间调用 deallocate 所失效。n 必须与先前传递给 allocate 的值匹配或介于通过 allocate_at_least 请求和返回的元素数量之间(可以等于任一边界) (C++23 起)。不抛出异常。 |
a.max_size() | A::size_type | 可以传递给 A::allocate() 的最大值。 |
a.construct(xp, args) | (未使用) | 在 xp 指向的先前分配的存储中构造一个类型 X 的对象,使用 args 作为构造函数参数。 |
a.destroy(xp) | (未使用) | 销毁 xp 指向的类型 X 的对象,但不解除分配任何存储。 |
实例间的关系
表达式 | 返回类型 | 要求 |
---|---|---|
a1 == a2 | bool | * 仅当由分配器 a1 分配的存储可以通过 a2 解除分配时才为 true。* 建立自反、对称和传递关系。 * 不抛出异常。 |
a1 != a2 | bool | * 与 !(a1 == a2) 相同。 |
声明 | 效果 | 要求 |
---|---|---|
A a1(a) | 复制构造 a1 ,使得 a1 == a 。(注意:每个分配器也都满足 CopyConstructible。) | * 不抛出异常。 |
A a1 = a | 同上 | * 不抛出异常。 |
A a(b) | 构造 a ,使得 B(a) == b 和 A(b) == a 。(注意:这意味着通过重新绑定相关的所有分配器都维护彼此的资源,例如内存池。) | * 不抛出异常。 |
A a1(std::move(a)) A a1 = std::move(a) | 构造 a1 ,使其等于 a 的先前值。 | * 不抛出异常。 * a 的值不变,且 a1 == a 。 |
A a(std::move(b)) | 构造 a ,使其等于 A(b) 的先前值。 | * 不抛出异常。 |
类型 ID | 别名类型 | 要求 |
---|---|---|
A::is_always_equal | std::true_type 或 std::false_type 或派生自此类。 | * 如果类型 A 的任意两个分配器总是比较相等,则为 true。 * (如果未提供,std::allocator_traits 默认此项为 std::is_empty<A>::type。) |
对容器操作的影响
表达式 | 返回类型 | 描述 |
---|---|---|
a.select_on_container_copy_construction() | A | * 提供一个 A 实例,供从当前使用 a 的容器复制构造的容器使用。* (通常返回 a 的副本或默认构造的 A。) |
类型 ID | 别名类型 | 描述 |
---|---|---|
A::propagate_on_container_copy_assignment | std::true_type 或 std::false_type 或派生自此类。 | * 如果使用类型 A 的分配器的容器在复制赋值时需要复制分配器,则为 std::true_type 或派生自它。 * 如果此成员是 std::true_type 或派生自它,则 A 必须满足 CopyAssignable,并且复制操作不得抛出异常。 * 请注意,如果源容器和目标容器的分配器不比较相等,则复制赋值必须使用旧分配器解除分配目标的内存,然后使用新分配器分配内存,然后才复制元素(和分配器)。 |
A::propagate_on_container_move_assignment | std::true_type 或 std::false_type 或派生自此类。 | * 如果使用类型 A 的分配器的容器在移动赋值时需要移动分配器,则为 std::true_type 或派生自它。 * 如果此成员是 std::true_type 或派生自它,则 A 必须满足 MoveAssignable,并且移动操作不得抛出异常。 * 如果未提供此成员或派生自 std::false_type 且源容器和目标容器的分配器不比较相等,则移动赋值无法取得源内存的所有权,并且必须单独移动赋值或移动构造元素,并根据需要调整自身内存的大小。 |
A::propagate_on_container_swap | std::true_type 或 std::false_type 或派生自此类。 | * 如果使用类型 A 的分配器的两个容器在交换时需要交换分配器,则为 std::true_type 或派生自它。 * 如果此成员是 std::true_type 或派生自它,则 A 的左值必须是 Swappable,并且交换操作不得抛出异常。 * 如果未提供此成员或派生自 std::false_type 且两个容器的分配器不比较相等,则容器交换的行为未定义。 |
注意:
[1] 另见下面的 花哨指针。
[2] rebind 只有在此分配器是 SomeAllocator<T, Args> 形式的模板时才可选(由 std::allocator_traits 提供),其中 Args 是零个或更多附加模板类型参数。
给定
x1
和x2
,类型 X::void_pointer、X::const_void_pointer、X::pointer 或 X::const_pointer 的(可能不同)对象。那么,当且仅当x1
和x2
都可以使用仅包含这四种类型的 static_cast 序列显式转换为类型 X::const_pointer 的两个相应对象px1
和px2
,并且表达式px1 == px2
的计算结果为 true 时,x1
和x2
是等效值的指针值。
给定
w1
和w2
,类型 X::void_pointer 的对象。那么,对于表达式w1 == w2
和w1 != w2
,可以替换其中一个或两个对象为类型 X::const_void_pointer 的 等效值 对象,而语义不变。
给定
p1
和p2
,类型 X::pointer 的对象。那么,对于表达式p1 == p2
,p1 != p2
,p1 < p2
,p1 <= p2
,p1 >= p2
,p1 > p2
,p1 - p2
,可以替换其中一个或两个对象为类型 X::const_pointer 的 等效值 对象,而语义不变。
上述要求使得比较 Container 的迭代器和 const_iterators 成为可能。
分配器完整性要求 (C++17 起)
如果以下两项都为真,则类型 T 的分配器类型 X 额外满足分配器完整性要求,无论 T 是否是完整类型
- X 是完整类型
- 除了 value_type,std::allocator_traits<X> 的所有成员类型都是完整类型。
有状态和无状态分配器
每种分配器类型都是有状态或无状态的。通常,有状态分配器类型可以具有表示不同内存资源的不等值,而无状态分配器类型表示单个内存资源。
尽管不要求自定义分配器是无状态的,但标准库中使用有状态分配器的方式和方式是实现定义的。如果实现不支持此类用法,使用不等的分配器值可能导致实现定义的运行时错误或未定义行为。 (直到 C++11)
自定义分配器可能包含状态。每个容器或其他分配器感知对象都存储一个提供的分配器实例,并通过 std::allocator_traits 控制分配器替换。 (自 C++11 起)
无状态分配器类型的实例总是比较相等。无状态分配器类型通常实现为空类,适用于空基类优化。
std::allocator_traits 的成员类型 is_always_equal 旨在用于确定分配器类型是否是无状态的。 (自 C++17 起)
花哨指针
当成员类型 pointer 不是原始指针类型时,它通常被称为“花哨指针”。引入此类指针是为了支持分段内存架构,如今它们用于访问与原始指针访问的同质虚拟地址空间不同的地址空间中分配的对象。花哨指针的一个例子是映射地址无关指针 boost::interprocess::offset_ptr,它使得在共享内存和内存映射文件中分配基于节点的数据结构(例如 std::set)成为可能,这些文件在每个进程中映射到不同的地址。花哨指针可以独立于提供它们的分配器使用,通过类模板 std::pointer_traits (C++11 起)。函数 std::to_address 可以用于从花哨指针获取原始指针。 (C++20 起)
标准库中花哨指针和自定义大小/不同类型的使用是条件支持的。实现可能要求成员类型 pointer
、const_pointer
、size_type
和 difference_type
分别是 value_type*
、const value_type*
、std::size_t 和 std::ptrdiff_t。 (直到 C++11)
标准库
以下标准库组件满足分配器要求
pub | 分配器 | 默认分配器 |
pub | scoped_allocator_adaptor(C++11) | 为多级容器实现多级分配器 |
pub | polymorphic_allocator(C++17) | 一种分配器,它根据构造时提供的 std::pmr::memory_resource 支持运行时多态性 |
示例
展示了一个 C++11 分配器,除了为匹配 C++20 风格而添加的 [[nodiscard]]。
#include <cstdlib>
#include <new>
#include <limits>
#include <iostream>
#include <vector>
template<class T>
struct Mallocator
{
typedef T value_type;
Mallocator () = default;
template<class U>
constexpr Mallocator (const Mallocator <U>&) noexcept {}
[[nodiscard]] T* allocate(std::size_t n)
{
if (n > std::numeric_limits<std::size_t>::max() / sizeof(T))
throw std::bad_array_new_length();
if (auto p = static_cast<T*>(std::malloc(n * sizeof(T))))
{
report(p, n);
return p;
}
throw std::bad_alloc();
}
void deallocate(T* p, std::size_t n) noexcept
{
report(p, n, 0);
std::free(p);
}
private:
void report(T* p, std::size_t n, bool alloc = true) const
{
std::cout
<< (alloc ? "Alloc: " : "Dealloc: ") << sizeof(T) * n
<< " bytes at " << std::hex << std::showbase
<< reinterpret_cast<void*>(p) << std::dec << '\n';
}
};
template<class T, class U>
bool operator==(const Mallocator <T>&, const Mallocator <U>&) {
return true;
}
template<class T, class U>
bool operator!=(const Mallocator <T>&, const Mallocator <U>&) {
return false;
}
int main()
{
std::vector<int, Mallocator<int>> v(8);
v.push_back(42);
}
Alloc: 32 bytes at 0x2020c20
Alloc: 64 bytes at 0x2023c60
Dealloc: 32 bytes at 0x2020c20
Dealloc: 64 bytes at 0x2023c60
缺陷报告
以下改变行为的缺陷报告已追溯应用于先前发布的 C++ 标准。
DR | 应用于 | 发布时的行为 | 正确行为 |
---|---|---|---|
LWG 179 | C++98 | pointer 和 const_pointer 不需要相互比较 | 必需 |
LWG 199 | C++98 | a.allocate(0) 的返回值不明确 | 未指定 |
LWG 258 (N2436) | C++98 | 分配器之间的相等关系不要求自反、对称或传递 | 要求自反、对称和传递 |
LWG 274 | C++98 | T 可以是 const 限定类型或引用类型,可能导致 std::allocator 格式错误 [1] | 禁止这些类型 |
LWG 2016 | C++11 | 分配器的复制、移动和交换操作在使用时可能会抛出异常 | 要求不抛出 |
LWG 2108 | C++11 | 无法显示分配器是无状态的 | 提供了 is_always_equal |
LWG 2263 | C++11 | LWG 问题 179 的解决方案在 C++11 中意外遗漏,并且未推广到 void_pointer 和 const_void_pointer | 恢复并推广 |
LWG 2447 | C++11 | T 可以是 volatile 限定对象类型 | 禁止这些类型 |
LWG 2593 | C++11 | 从分配器移动可能会修改其值 | 禁止修改 |
P0593R6 | C++98 | allocate 不需要在其分配的存储中创建数组对象 | 必需 |
[1] std::allocator 的成员类型 reference 和 const_reference 分别定义为 T& 和 const T&。