C++ 具名要求:分配器 (Allocator)

来自cppreference.com
< cpp‎ | named req
 
 
C++ 具名要求
基础
类型属性
库所属
(C++11)
Allocator
容器
容器元素
(C++11)

迭代器
流 I/O
格式化
(C++20)
随机数
(C++11)    
并发
(C++11)
(C++11)
范围
其他
(C++11)


 

封装访问/寻址,分配/解分配,以及对象的构造/析构的策略。

可能需要分配或释放内存的每个标准库组件,从 std::stringstd::vector 和除 std::array 以外的所有容器,到 std::shared_ptrstd::function,都通过分配器 (Allocator) 进行这些操作:分配器是满足下列要求的类类型对象。

许多分配器要求的实现是可选的,因为所有知分配器类,包括标准库容器,都通过 std::allocator_traits 访问分配器,而 std::allocator_traits 提供这些要求的默认实现。

要求

给定

  • T无 const 限定的非引用类型 (C++11 前)无 cv 限定的对象类型 (C++11 起)
  • AT 类型的分配器 (Allocator) 类型
  • aA 类型对象
  • B,某个无 cv 限定的对象类型 U 的对应分配器 (Allocator) 类型(由重绑定 A 获得)
  • bB 类型对象
  • pallocator_traits<A>::pointer 类型值,由调用 allocator_traits<A>::allocate() 获得
  • cpallocator_traits<A>::const_pointer 类型值,由从 p 转换获得
  • vpallocator_traits<A>::void_pointer 类型值,由从 p 转换获得
  • cvpallocator_traits<A>::const_void_pointer 类型值,由从 cp 或从 vp 转换获得
  • xp,指向某个无 cv 限定类型 X 的可解引用的指针
  • r,由表达式 *p 获得的 T 类型左值
  • nallocator_traits<A>::size_type 类型值
内部类型
类型标识 别名使用的类型 要求
A::pointer (可选) (未指明)[1]
A::const_pointer (可选) (未指明)
A::void_pointer (可选) (未指明)
A::const_void_pointer (可选) (未指明)
  • 满足可空指针 (NullablePointer)
  • A::pointerA::const_pointerA::void_pointer 可以转换到 A::const_void_pointer
  • B::const_void_pointerA::const_void_pointer 是同一类型。
A::value_type T
A::size_type (可选) (未指明)
  • 无符号整数类型。
  • 能表示 A 能分配的最大对象的大小。
A::difference_type (可选) (未指明)
  • 有符号整数类型。
  • 能表示任何两个指向 A 所分配的对象的指针的差
A::template rebind<U>::other
(可选)[2]
B
  • 对于任何 UB::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.allocate(n),但可能以未指定的方式使用 cvpnullptr 或从 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 指向的存储,该值必须由之前调用 allocate allocate_at_least (C++23 起) 返回且未被中间对 deallocate 的调用非法化。n 必须匹配先前传给 allocate 的值或在经由 allocate_at_last 请求和返回的元素数之间(可以等于任一边界) (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
  • !(a1 == a2)
声明 效果 要求
A a1(a) 复制构造 a1 使得 a1 == a
(注:每个分配器 (Allocator) 也满足可复制构造 (CopyConstructible) 。)
  • 不会抛出异常。
A a1 = a
A a(b) 构造 a 使得 B(a) == bA(b) == a
(这隐含所有由 rebind 联系的分配器均维护彼此的资源,例如内存池。)
  • 不会抛出异常。
A a1(std::move(a)) 构造 a1 使得它等于先前 a 的值。
  • 不会抛出异常。
  • 不会更改 a 的值且 a1 == a
A a1 = std::move(a)
A a(std::move(b)) 构造 a 使得它等于先前 A(b) 的值。
  • 不会抛出异常。
类型标识 别名使用的类型 要求
A::is_always_equal
(可选)
std::true_typestd::false_type 或从它们派生。
容器操作上的影响
表达式 返回类型 描述
a.select_on_container_copy_construction()
(可选)
A
  • 提供从当前使用 a 的容器复制构造容器所用的 A 的实例。
  • (通常返回 a 的副本或默认构造的 A。)
类型标识 别名使用的类型 描述
A::propagate_on_container_copy_assignment
(可选)
std::true_typestd::false_type 或从它们派生。
  • 如果复制赋值使用 A 类型分配器的容器时需要复制它那么是 std::true_type 或它的派生类。
  • 如果此成员是 std::true_type 或从它派生,那么 A 必须满足可复制赋值 (CopyAssignable) 且复制操作不能抛出异常。
  • 注意如果源与目标容器的分配器不比较相等,那么复制赋值必须用旧分配器解分配目标的内存,然后在复制元素(和分配器)前用新分配器分配内存。
A::propagate_on_container_move_assignment
(可选)
  • 如果移动赋值使用 A 类型分配器的容器时需要移动它那么是 std::true_type 或它的派生类。
  • 如果此成员是 std::true_type 或从它派生,那么 A 必须满足可移动赋值 (MoveAssignable) 且移动操作不能抛出异常。
  • 如果不提供此成员或它从 std::false_type 派生,而源与目标容器的分配器不比较相等,那么移动赋值不能取得源内存的所有权,并且必须单独地复制赋值或复制构造元素,按需重置其自身内存的大小。
A::propagate_on_container_swap
(可选)
  • 如果交换两个使用 A 类型分配器的容器时需要移动它们那么是 std::true_type 或它的派生类。
  • 如果此成员是 std::true_type 或从它派生,那么 A 的左值必须为可交换 (Swappable) 且交换操作不能抛出异常。
  • 如果不提供此成员或它从 std::false_type 派生,且两个容器的分配器不比较相等,那么容器交换的行为未定义。

注:

  1. 参阅后述缀饰指针
  2. rebind 只有在分配器是形式为 SomeAllocator<T, Args> 的模板,其中 Args 是零或更多个额外的类型模板形参才不需要(由 std::allocator_traits 提供)。

给定

  • x1x2,(可能不同)类型 X::void_pointerX::const_void_pointerX::pointerX::const_pointer 的对象。

那么 x1x2等价值的指针值,当且仅当 x1x2 能用 static_cast,仅使用这四个类型的序列显式转换成两个对应的 X::const_pointer 类型对象 px1px2,而表达式 px1 == px2 求值为 true

给定

  • w1w2X::void_pointer 类型对象。

那么对于表达式 w1 == w2w1 != w2,可以将一个或两个对象替换成等价值X::const_void_pointer 类型对象而无语义更改。

给定

  • p1p2X::pointer 类型对象

那么对于表达式 p1 == p2p1 != p2p1 < p2p1 <= p2p1 >= p2p1 > p2p1 - p2,可以将一个或两个对象替换成等价值X::const_pointer 类型对象而无语义更改。

以上要求使得能比较容器 (Container) iteratorconst_iterator

分配器完整性要求

如果满足以下所有条件,那么无论 T 是否为完整类型,针对类型 T 的分配器类型 X 还额外满足分配器完整性要求

  • X 是完整类型
  • value_type 之外,所有 std::allocator_traits<X> 的成员都是完整类型。
(C++17 起)

有状态与无状态分配器

每个分配器 (Allocator) 类型要么是有状态要么是无状态的。通常来说,有状态分配器类型能拥有代表有别的内存资源的不相等值,而无状态分配器类型代表单一内存资源。

尽管不要求自定义分配器为无状态,标准库中是否及如何支持分配器由实现定义。如果实现不支持使用不相等的分配器值,那么这种使用可能导致实现定义的运行时错误或未定义行为。

(C++11 前)

定制分配器可含有状态。每个容器或另一知分配器对象存储提供的分配器的实例并通过 std::allocator_traits 控制分配器替换。

(C++11 起)

无状态分配器类型的实例始终比较相等。无状态分配器类型常实现为空类并适合空基类优化

std::allocator_traits 的成员类型 is_always_equal 有意用于确定分配器类型是否为无状态。

(C++17 起)

缀饰指针

当成员类型 pointer 不是原生指针时,它通常被称为“缀饰指针(fancy pointer)”。这种指针曾为支持分段内存架构而引入,并在当今用于访问在某些不同于原生指针所访问的同质虚拟地址空间的地址空间中所分配的对象。缀饰指针的一个实例是映射的不依赖地址指针 boost::interprocess::offset_ptr,它使得在共享内存和在每个进程中映射到不同地址的映射到内存文件中,分配 std::set 一类的基于结点的数据结构可行。通过类模板 std::pointer_traits (C++11 起)可以独立于提供缀饰指针的分配器而使用它们。能用函数 std::to_address 从缀饰指针获得裸指针。 (C++20 起)

在标准库中使用缀饰指针和定制的大小/差类型是条件性支持的。实现可以要求成员类型 pointerconst_pointersize_typedifference_type 分别是 value_type*const value_type*std::size_tstd::ptrdiff_t

(C++11 前)

标准库

下列标准库组件满足分配器 (Allocator) 要求:

默认的分配器
(类模板)
为多级容器实现的多级分配器
(类模板)
std::memory_resource 构造,支持基于它的运行时多态的分配器
(类模板)

示例

一个 C++11 分配器,但添加了 [[nodiscard]] 以符合 C++20 风格。

#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 << "在 " << std::hex << std::showbase
                  << reinterpret_cast<void*>(p) << std::dec
                  << (alloc ? " 分配 " : " 解分配 ")
                  << sizeof(T) * n << " 个字节\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);
}

可能的输出:

在 0x2020c20 分配 32 个字节
在 0x2023c60 分配 64 个字节
在 0x2020c20 解分配 32 个字节
在 0x2023c60 解分配 64 个字节

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
LWG 179 C++98 未要求 pointerconst_pointer 可相互比较 已要求
LWG 199 C++98 a.allocate(0) 的返回值不明确 此时返回值未指定
LWG 258 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_pointerconst_void_pointer
恢复并推广
LWG 2447 C++11 T 可以是有 volatile 限定的对象类型 禁止这些类型
LWG 2593 C++11 从分配器移动可能修改它的值 禁止修改
P0593R6 C++98 未要求 allocate 在其所分配的存储中创建数组 已要求
  1. std::allocator 的成员类型 referenceconst_reference 分别定义为 T&const T&
    • 如果 T 是引用类型,那么 referenceconst_reference 会因为无法组成到引用的引用而非良构(引用折叠在 C++11 中引入)
    • 如果 T 具有 const 限定,那么 referenceconst_reference 表示相同的类型,导致 address() 的重载集非良构