在C++编程中,分配器(英语:allocator)是C++标准库的重要组成部分。C++的库中定义了多种被统称为“容器”的数据结构(如链表、集合等),这些容器的共同特征之一,就是其大小可以在程序的运行时改变;为了实现这一点,进行动态内存分配就显得尤为必要,在此分配器就用于处理容器对内存的分配与释放请求。换句话说,分配器用于封装标准模板库(STL)容器在内存管理上的低层细节。默认情况下,C++标准库使用其自带的通用分配器,但根据具体需要,程序员也可自行定制分配器以替代之。
分配器最早由亚历山大·斯特潘诺夫(英语:Alexander Stepanov)作为C++标准模板库(Standard Template Library,简称STL)的一部分发明,其初衷是创造一种能“使库更加灵活,并能独立于底层数据模型的方法”,并允许程序员在库中利用自定义的指针和引用类型(英语:Reference (C++));但在将标准模板库纳入C++标准时,C++标准委员会意识到对数据模型的完全抽象化处理会带来不可接受的性能损耗,为作折中,标准中对分配器的限制变得更加严格,而有鉴于此,与斯特潘诺夫原先的设想相比,现有标准所描述的分配器可定制程度已大大受限。
虽然分配器的定制有所限制,但在许多情况下,仍需要用到自定义的分配器,而这一般是为封装对不同类型内存空间(如共享内存与已回收内存)的访问方式,或在使用内存池进行内存分配时提高性能而为。除此以外,从内存占用和运行时间的角度看,在频繁进行少量内存分配的程序中,若引入为之专门定制的分配器,也会获益良多。
亚历山大·斯特潘诺夫与李梦(Meng Lee)在1994年将标准模板库草案提交给C++标准委员会。提交伊始,草案就得到了委员会的初步支持,但委员会成员也对此提出了一些意见,尤其是要求斯特潘诺夫定制库内的容器,使之与底层存储模型相独立。作为对要求的回应,斯特潘诺夫发明了分配器,而正因此,标准模板库的所有容器接口也被迫重写,以与分配器相兼容。在修改标准模板库以将之引入C++标准库的过程中,许多标准委员会成员(如安德鲁·克尼格(英语:Andrew Koenig (programmer))与比雅尼·斯特劳斯特鲁普)也与斯特潘诺夫协同工作。他们亦发现自定义分配器甚至有应用于长生命周期(持续存储)的标准模板库容器的潜力,斯特潘诺夫对此的评论则是“重要而有趣的见解”。
在原有的提案里的分配器设定中,斯特潘诺夫杂糅了一些语言特性(如可将模板参数也定义为模板),但由于当时的编译器皆无法处理之,所以最终并未被标准委员会所接纳,斯特潘诺夫则如此描述当时的情形:“比雅尼·斯特劳斯特鲁普与安迪·克尼格需要花大量时间来检查我们是否正确使用了这些未实现的特性。”在分配器应用后,之前库中直接使用的指针与引用类型(英语:Reference (C++))也可以分配器所定义的类型替代,斯特潘诺夫亦曾如此描述分配器:“标准模板库有个不错的特性便是:唯一要提及机器相关类型的地方(……)(只需)被封装成(仅)约16行内的代码。”除此以外,斯特潘诺夫原本还打算在分配器中完全封装存储模型,但标准委员会意识到这一做法会造成无法接受的性能损失,因而为补偿之,分配器的使用需求也做了一定扩充。
分配器的应用中比较特别的一点是,容器的实现过程中可能会假定分配器对指针与相关整型的类型定义与默认分配器所提供的等价,因而给定分配器类型的所有实例在比较时常会得出“相等”的结果,而这一效果实际上恰与设计分配器的初衷背道而驰,并使带状态分配器的可用性大大受限,斯特潘诺夫后来对此评论道:“(分配器)理论上说是不差的主意(……)但不幸的是在实践中无法发挥其功效。“他洞察到若要令分配器更加实用,就有必要针对核心语言的引用(英语:Reference (C++))部分进行修改。
任意满足分配器使用需求的C++类都可作分配器使用。具体来说,当一个类(在此设为类A)有为一个特定类型(在此设为类型T)的对象分配内存的能力时,该类就必须提供以下类型的定义:
如此才能以通用的方式声明对象与对该类对象的引用T。allocator提供这些指针或引用的类型定义的初衷,是隐蔽指针或引用的物理实现细节;因为在16位编程时代,(far pointer)是与普通指针非常不同的,allocator可以定义一些结构来表示这些指针或引用,而容器类用户不需要了解其是如何实现的。
虽然按照标准,在库的实现过程中允许假定分配器(类)A的A::pointer(指针)与A::const_pointer(常量指针)即是对T*与T const*的简单的类型定义,但一般更鼓励支持通用分配器。
另外,设有对于为某一对象类型T所设定的分配器A,则A必须包含四项成员函数,分别为分配函数、解除分配函数、最大个数函数和地址函数:
除此以外,由于对象的构造/析构过程与分配/解除分配过程分别进行 ,因而分配器还需要成员函数A::construct(构造函数)与A::destroy(析构函数)以对对象进行构造与析构,且两者应等价于如下函数:
template <typename T>void A::construct(A::pointer p, A::const_reference t) { new ((void*) p) T(t); }template <typename T>void A::destroy(A::pointer p){ ((T*)p)->~T(); }