这篇专栏文章将讨论你能用allocator来做什么以及如何定义一个自己的版本。我只会讨论C++标准所定义的allocator:引入准标准时代的设计,或试图绕过有缺陷的编译器,只会增加混乱。
C++标准中的Allocator分成两块:一个通用需求集(描述于§ 20.1.5(表 32)),和叫std::allocator的class(描述于§20.4.1)。如果一个class满足表32的需求,我们就称它为一个 allocator。std::allocator类满足那些需求,因此它是一个allocator。它是标准程序库中的唯一一个预先定义 allocator类。
每个 C++程序员都已经知道动态内存分配:写下new X来分配内存和创建一个X类型的新对象,写下delete p来销毁p所指的对象并归还其内存。你有理由认为allocator会使用new和delete--但它们没有。(C++标准将::operator new()描述为“allocation function”,但很奇怪,allocator并不是这样的。)
有关allocator的最重要的事实是它们只是为了一个目的:封装STL容器在内存管理上的低层细节。你不应该在自己的代码中直接调用allocator 的成员函数,除非正在写一个自己的STL容器。你不应该试图使用allocator来实现operator new[];这不是allocator该做的。 如果你不确定是否需要使用allocator,那就不要用。
allocator 是一个类,有着叫allocate()和deallocate()成员函数(相当于malloc和free)。它还有用于维护所分配的内存的辅助函数和指示如何使用这些内存的typedef(指针或引用类型的名字)。如果一个STL容器使用用户提供的allocator来分配它所需的所有内存(预定义的 STL容器全都能这么做;他们都有一个模板参数,其默认值是std::allocator),你就能通过提供自己的allocator来控制它的内存管理。
这种柔性是有限制的:仍然由容器自己决定它将要申请多少内存以及如何使用它们。在容器申请更多的内存时,你能控制它调用那个低层函数,但是你不能通过使用 allocator来让一个vector行动起来像一个deque一样。虽然如此,有时候,这个受限的柔性也很有用。比如,假设你有一个特殊的 fast_allocator,能快速分配和释放内存(也许通过放弃线程安全性,或使用一个小的局部堆),你能通过写下std::list<T, fast_allocator<T> >而不是简单的std::list<T>来让标准的list使用它。
如果这看起来对你很陌生,那就对了。没有理由在常规代码中使用allocator的。
以一个简单的例子开始。根据C++标准,std::allocator构建在::operator new()上。如果你正在使用一个自动化内存使用跟踪工具,让std::allocator更简单些会更方便。我们可以用malloc()代替:: operator new(),而且我们也不考虑(在好的std::allocator实作中可以找到的)复杂的性能优化措施。我们将这个简单的allocator叫作 malloc_allocator 。
既然malloc_allocator的内存管理很简单,我们就能将重点集中在所有STL的allocator所共有的样板上。首先,一些类型: allocator是一个类模板,它的实例专为某个类型T分配内存。我们提供了一序列的typedef,以描述该如何使用此类型的对象: value_type指T本身,其它的则是有各种修饰字的指针和引用。
template <class T> class malloc_allocator
{
public:
typedef T value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
...
};
为什么有这么多的typedef?你可能认为pointer是多余的:它就是value_type *。绝大部份时候这是对的,但你可能有时候想定义非传统的allocator,它的pointer是一个pointer-like的class,或非标的厂商特定类型value_type __far *;allocator是为非标扩展提供的标准hook。不寻常的pointer类型也是存在address()成员函数的理由,它在 malloc_allocator中只是operator &()的另外一种写法:
template <class T> class malloc_allocator
{
public:
pointer address(reference x) const { return &x; }
const_pointer address(const_reference x) const {
return &x;
}
...
};
我们也传给deallocate()两个参数:当然一个是指针,但同样还有一个元素计数值。容器必须自己掌握大小信息;传给allocate和 deallocate的大小参数必须匹配。同样,这个额外的参数是为效率而存在的,而同样,malloc_allocator不使用它。
template <class T> class malloc_allocator
{
public:
pointer allocate(size_type n, const_pointer = 0) {
void* p = std::malloc(n * sizeof(T));
if (!p)
throw std::bad_alloc();
return static_cast<pointer>(p);
}
void deallocate(pointer p, size_type) {
std::free(p);
}
size_type max_size() const {
return static_cast<size_type>(-1) / sizeof(value_type);
}
...
};
C+ +语言提供一个机制以在特定的内存位置创建对象:placement new。如果你写下new(p) T(a, b),那么你正在调用T的构造函数产生一个新的对象,一如你写的new T(a, b)或 T t(a, b)。区别是当你写new(p) T(a, b),你指定了对象被创建的位置:p所指向的地址。(自然,p必须指向一块足够大的内存,而且必须是未被使用的内存;你不能在相同的地址构建两个不同的对象。)。你也可以调用对象的析构函数,而不释放内存,只要写p->~T()。
这些特性很少被使用,因为通常内存的分配和初始化是一起进行的:使用未初始化的内存是不方便的和危险的。你需要如此低层的技巧的很少几处之一就是你在写一个容器类,于是allocator将内存的分配与初始化解耦。成员函数construct()调用placement new,而且成员函数destory()调用析构函数。
template <class T> class malloc_allocator
{
public:
void construct(pointer p, const value_type& x) {
new(p) value_type(x);
}
void destroy(pointer p) { p->~value_type(); }
...
};
这些成员函数没有一个是static的,因此,容器在使用allocator前做的第一件事就是创建一个allocator对象--也就是说我们应该定义一些构造函数。但是,我们不需要赋值运算:一旦容器创建了它的allocator,这个allocator就从没想过会被改变。表32中的对 allocator的需求没有包括赋值。只是基于安全,为了确保没人偶然使用了赋值运算,我们将禁止掉这个可能自动生成的函数。
template <class T> class malloc_allocator
{
public:
malloc_allocator() {}
malloc_allocator(const malloc_allocator&) {}
~malloc_allocator() {}
private:
void operator=(const malloc_allocator&);
...
};
template <class T>
inline bool operator==(const malloc_allocator<T>&,
const malloc_allocator<T>&) {
return true;
}
tmplate <class T>
inline bool operator!=(const malloc_allocator<T>&,
const malloc_allocator<T>&) {
return false;
}
最后,我们到了allocator的定义体中一个微妙的部份:在不同的类型之间映射。问题是,一个allocator类,比如 malloc_allocator<int>,全部是围绕着单个value_type构建的:malloc_allocator< int>::pointer是int*,malloc_allocator<int>().allocate(1)返回足够容纳一个 int对象的内存,等等。然而,通常,容器类使用malloc_allocator可能必须处理超过一个类型。比如,一个list类,不分配int对象;实际上,它分配list node对象。(我们将在下一段落研究细节。)于是,当你创建一个std::list<int, malloc_allocator<int> >时,list必须将malloc_allocator<int>转变成为处理list_node类型的 malloc_allocator。
这个机制称为重绑定,它有二个部份。首先,对于给定的一个value_type是X1的allocator类型A1,你必须能够写出一个allocator 类型A2,它与A1完全相同,除了value_type是X2。其次,对于给定的A1类型的对象a1,你必须能够创建一个等价的A2类型对象a2。这两部分都使用了成员模板,这也就是allocator不能被老的编译器支持,或支持得很差的原因。
template <class T> class malloc_allocator
{
public:
template <class X>
malloc_allocator(const malloc_allocator<X>&) {}
template <class X>
struct rebind { typedef malloc_allocator<X> other; };
...
};
如果有一个allocator类型A1,对应于另外一个value_type的类型是typename A1::template rebind<X2>::othere[注2]。正如你能将一个类型转换为另外一个,模板的转换用构造函数让你转换值:你可以写 malloc_allocator<int>(a),无论a的类型是malloc_allocator<int>,或 malloc_allocator<double>,或malloc_allocator<string>。象往常一样, malloc_allocator的转换用构造函数用不着做任何事,因为malloc_allocator没有成员变量。
附带一提,虽然绝大多数的allocator有一个模板参数(allocator的value_type),但这不是需求中的规定,只不过常常正巧如此。重绑定机制在多模板参数的allocator上也工作良好:
template <class T, int flags> class my_allocator
{
public:
template <class U>
struct rebind { typedef my_allocator<U, flags> other; };
...
};
template<> class malloc_allocator<void>
{
typedef void value_type;
typedef void* pointer;
typedef const void* const_pointer;
template <class U>
struct rebind { typedef malloc_allocator<U> other; };
};
就是它了!malloc_allocator的完备源码见Listing 1。
std::vector<char, malloc_allocator<char> > V;
取代简单的std::vector<char>,或用
typedef std::list<int, mempool_allocator<int> > List;
List L(mempool_allocator<int>(p));
取代简单的std::list<int>。
但是你能做得更多。STL的卖点是它是可扩展性:正如你能写自己的allocator,你也能写你自己的容器类。如果你很小心, 并且你写的容器类使用它的allocator来处理所有的内存相关操作,那么别人将能够加入他们自己的用户自定义allocator。
诸如std::vector和std::list这样的容器类是很复杂的,而且大部分复杂性与内存管理无关。让我们以一个简单的例子开始,这样我们可以只关注于allocator。考虑一个固定大小数组的类,Array,元素的个数是在构造函数中设定的,并且在此之后不会改变。(这有点像std:: valarray的一个简化版本。)它有二个模板参数,元素类型和allocator类型。
容器,和allocator一样,以巢式类型申明开始:value_type, reference,const_reference,size_type,difference_type,iterator,和 const_iterator。通常,这些类型中的绝大部分都可以直接从它的allocator中获得--这也解释了为什么容器的value_type必须和allocator中的相匹配。
当然,iterator类型通常不来自于allocator;通常iterator是一个类,完全取决于容器的内在表示。Array类比通常见到的容器简单,因为它实际上将所有元素存储在单块连续内存中;我们只要维护指向内存块开始和结束处的两个指针。iterator就是指针。
在更进一步之前,我们必须决定:我们将怎样储存allocator?构造函数将接受一个allocator对象作为参数。我们必须在容器的整个生命期内保存它的一个拷贝,因为在析构函数中还需要它。
依感觉,这儿没什么问题:我们只要申明一个Allocator类型的成员变量,然后使用它。方法是正确的,但不爽。毕竟,在99%的时间里,用户都不想考虑有关allocator的事;他们只会写Array<int>并使用默认值--而默认的allocator可能是一个没有任何非static 成员变量的空类。问题是即使Allocator是一个空类,这样的成员变量也会有开销。(这是C++标准所要求的。) 我们的Array类将会有三个word的开销,而不是两个。也许一个word的额外开销不是大问题,但总是不爽,它迫使所有用户为一个几乎从不使用的功能承担了开销。
都很多方法来解决这个问题,其中一些使用了traits类和偏特化。或许最简单的解决方法就是使用(私有)继承而不是成员变量。编译器被允许优化掉空基类,而且时下绝大多数的编译器都这么做了。
我们最终能写下定义的骨架了:
template <class T, class Allocator = std::allocator<T> >
class Array : private Allocator
{
public:
typedef T value_type;
typedef typename Allocator::reference reference;
typedef typename Allocator::const_reference
const_reference;
typedef typename Allocator::size_type size_type;
typedef typename Allocator::difference_type
difference_type;
typedef typename Allocator::pointer iterator;
typedef typename Allocator::const_pointer const_iterator;
typedef Allocator allocator_type;
allocator_type get_allocator() const {
return static_cast<const Allocator&>(*this);
}
iterator begin() { return first; }
iterator end() { return last; }
const_iterator begin() const { return first; }
const_iterator end() const { return last; }
Array(size_type n = 0,
const T& x = T(),
const Allocator& a = Allocator());
Array(const Array&);
~Array();
Array& operator=(const Array&);
private:
typename Allocator::pointer first;
typename Allocator::pointer last;
};
构造函数初始化allocator基类,获得足够容纳n个元素的一块内存(如果我们正在写类似于vector的东西,我们可能申请更大些的内存以供增长用),然后遍历内存以创建初始化值的拷贝。唯一的问题是异常安全:如果某一个元素的构造函数抛出了一个异常,我们必须撤销我们所做的一切。
template <class T, class Allocator>
Array<T, Allocator>::Array(size_type n,
const T& x,
const Allocator& a)
: Allocator(a), first(0), last(0)
{
if (n != 0) {
first = Allocator::allocate(n);
size_type i;
try {
for (i = 0; i < n; ++i) {
Allocator::construct(first + i, x);
}
}
catch(...) {
for(size_type j = 0; j < i; ++j) {
Allocator::destroy(first + j);
}
Allocator::deallocate(first, n);
throw;
}
}
}
析构函数比较简单,因为我们不需要担心异常安全:T的析构函数被假设为从不抛出异常。
template <class T, class Allocator>
Array<T, Allocator>::~Array()
{
if (first != last) {
for (iterator i = first; i < last; ++i)
Allocator::destroy(i);
Allocator::deallocate(first, last - first);
}
}
template <class T>
struct List_node
{
T val;
List_node* next;
};
l 使用一个value_type是List_node<T>的allocator,为一个新节点分配内存。 l 使用一个value_type是T的allocator,在节点的val位置上构建新的元素。 l 将这个节点连接到适当的位置。
这个过程需要处理两个不同的allocator,其中一个是通过对另一个的重绑定而获得的。它对几乎所有程序都工作得很好,即使是将allocator用于相当复杂的目的的程序。它所不能完成的是向allocator提供一些不寻常的指针类型。它明显依赖于能使用List_node<T> *类型的普通指针。
如果你极端野心勃勃,想将其它可能的指针类型传给allocator,一切都突然变得复杂多了--从一个节点指向另一个节点的指针不再是 List_node<T> *或void *了,但它必须是能够从allocator获取的某个类型。直接实现是不太可能的:用一个不完全的类型实例化allocator是非法的,因此,除非 List_node已经被完整定义,否则无法谈论指向List_node的指针。我们需要一个精巧的申明顺序。
template <class T, class Pointer>
struct List_node
{
T val;
Pointer next;
};
template <class T, class Alloc>
class List : private Alloc
{
private:
typedef typename Alloc::template rebind<void>::other
Void_alloc;
typedef typename Void_alloc::pointer Voidptr;
typedef typename List_node<T, Voidptr> Node;
typedef typename Alloc::template rebind<Node>::other
Node_alloc;
typedef typename Node_alloc::pointer Nodeptr;
typedef typename Alloc::template rebind<Voidptr>::other
Voidptr_alloc;
Nodeptr new_node(const T& x, Nodeptr next) {
Alloc a = get_allocator();
Nodeptr p = Node_alloc(a).allocate(1);
try {
a.construct(p->val, x);
}
catch(...) {
Node_alloc(a).deallocate(p, 1);
throw;
}
Voidptr_alloc(a).construct(p->next, Voidptr(next));
return p;
}
...
};
如果你想写一个如同malloc_allocator这样简单的allocator,应该没有困难。(前提是你正在使用一个比较现代的编译器)。然而,如果你的心比较大,--基于内存池的或支持非标指针类型的allocator--情况就比较不令人满意了。
如果你想使用可能的类指针(pointer-like)类型,它必须支持哪些操作?它必须有一个null值吗?如果是的话,那个值应该如何书写?你能使用类型转换吗?你如何在类指针对象与普通指针间进行转换?你必须要考虑操作指针时抛的异常吗?我在这最后一节提出了一些假设;C++标准没有指明这些假设的对错。这些细节都留给了具体的标准库的实现,即使某个实现完全忽略可选指针类型也是合法的。C++标准还遗留了一些没有答案的问题,比如当一个 allocator的不同实例不可互换时,将发生什么。
幸运的是,情形并不象标准(§ 20.1.5,第4-5段)中的词汇所描述得那么可怕。标准留下没有答案的问题,是因为在制订标准时,C++标准化委员会对答案达不成共识;对 allocator的必要的经验还不存在。每个参与制订的人都认为这是一个临时的补丁,其中的含糊肯定将在将来的修订中被去除。
等一下,如果你关心可移植性,最好还是远离可选的指针类型,而,如果你乐意接受一些限制,就能安全地使用诸如mempool_allocator这样不同对象实体间差别很大的allocator。所有主流标准库的实现现在都在某个方面不支持这样的allocator,而且不同实现间的差别不大。
正如容器接受一个allocator作为模板参数,容器的构造函数也接受一个allocator作为参数。容器保留这个参数的一份拷贝,并使用这个拷贝来处理所有的内存管理;容器的allocator一旦在构造函数中初始化完成,就终生不变。
唯一的问题是当你运行一个需要两个容器在内存管理上进行协同的操作时,会发生什么。在标准库中确实存在两个这样的操作:swap()(所有容器)和std::list::splice()。原理上,可以以这样几个不同的方式处理他们:
l 禁止在两个allocator不等价的容器间进行swap()和splice()。 l 在swap()和splice()中放入一个allocator等价性的测试。如果不等价,降格调用copy()和赋值运算。 l 只提供swap():如同对容器中的数据一样,交换它们的allocator。(很难找出如何将它推广到splice上.它同样也带来一个问题:如何swap没有定义赋值运算符的东西?)
如果你能避免对可能具有不同allocator的容器使用swap()和splice(),一切都很安全。在实践中,我没有发现这会造成严重束缚:你需要严格练习安全使用诸如内存池这类特性,而你或许并不想不加选择地混用使用不同allocator的容器。
部分因为不熟悉,部分因为C++标准的要求并不令人满意,目前对allocator的绝大多数的使用都很简单。由于C++社群对allocator变得越来越熟悉,并且标准已被阐明,我们能期待更复杂的使用将会浮现。 Listing 1: A sample allocator based on malloc
template <class T> class malloc_allocator
{
public:
typedef T value_type;
typedef value_type* pointer;
typedef const value_type* const_pointer;
typedef value_type& reference;
typedef const value_type& const_reference;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
template <class U>
struct rebind { typedef malloc_allocator<U> other; };
malloc_allocator() {}
malloc_allocator(const malloc_allocator&) {}
template <class U>
malloc_allocator(const malloc_allocator<U>&) {}
~malloc_allocator() {}
pointer address(reference x) const { return &x; }
const_pointer address(const_reference x) const {
return x;
}
pointer allocate(size_type n, const_pointer = 0) {
void* p = std::malloc(n * sizeof(T));
if (!p)
throw std::bad_alloc();
return static_cast<pointer>(p);
}
void deallocate(pointer p, size_type) { std::free(p); }
size_type max_size() const {
return static_cast<size_type>(-1) / sizeof(T);
}
void construct(pointer p, const value_type& x) {
new(p) value_type(x);
}
void destroy(pointer p) { p->~value_type(); }
private:
void operator=(const malloc_allocator&);
};
template<> class malloc_allocator<void>
{
typedef void value_type;
typedef void* pointer;
typedef const void* const_pointer;
template <class U>
struct rebind { typedef malloc_allocator<U> other; };
};
template <class T>
inline bool operator==(const malloc_allocator<T>&,
const malloc_allocator<T>&) {
return true;
}
template <class T>
inline bool operator!=(const malloc_allocator<T>&,
const malloc_allocator<T>&) {
return false;
}