您的位置:首页 > 编程语言 > Go语言

The Standard Librarian: What Are Allocators Good For?

2007-11-05 14:52 543 查看
Allocator是C++语言标准库中最神秘的部分之一。它们很少被显式使用,标准也没有明确出它们应该在什么时候被使用。今天的allocator与最初的STL建议非常不同,在此过程中还存在着另外两个设计--这两个都依赖于语言的一些特性,而直到最近才在很少的几个编译器上可用。对allocator的功能,标准似乎在一些方面追加了承诺,而在另外一些方面撤销了承诺。
这篇专栏文章将讨论你能用allocator来做什么以及如何定义一个自己的版本。我只会讨论C++标准所定义的allocator:引入准标准时代的设计,或试图绕过有缺陷的编译器,只会增加混乱。
什么时候不使用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的。
定义一个Allocator
关于allocator的这一点你已经看到了:它们是模板。Allocator,和容器一样,有value_type,而且allocator的value_type必须要匹配于使用它的容器的value_type。这有时会比较丑陋:map的value_type相当复杂,所以显式调用allocator的map看起来象这样的,std::map<K,V, fast_allocator<std::pair<const K, V> > >。(像往常一样,typedef会对此有帮助。)
以一个简单的例子开始。根据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;
...
};
这些类型与STL容器中的很相似,这不是巧合:容器类常常直接从它的allocator提取这些类型。
为什么有这么多的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;
}
...
};
现在我们能开始真正的工作:allocate()和deallocate()。它们很简单,但并不十分象malloc()和free()。我们传给allocate()两个参数:我们正在为其分派空间的对象的数目(max_size返回可能成功的最大请求值),以及可选的一个地址值(可以被用作一个位置提示)。象malloc_allocator这样的简单的allocator没有利用那个提示,但为高性能而设计的allocator可能会利用它。返回值是一个指向内存块的指针,它足以容纳n个value_type类型的对象并有正确的对齐。
我们也传给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);
}
...
};
allocate()和deallocate()成员函数处理的是未初始化的内存,它们不构造和销毁对象。语句a.allocate(1)更象malloc(sizeof(int))而不是new int。在使用从allocate()获得的内存前,你必须在这块内存上创建对象;在通过deallocate()归还内存前,你需要销毁那些对象。
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(); }
...
};
(为什么allocator有那些成员函数,什么时候容器可以直接使用placement new?一个理由是要隐藏笨拙的语法,而另一个是如果写一个更复杂的allocator时你可能想在构造和销毁对象时construct()和destroy()还有其它一些副作用。比如,allocator可能维护一个所有当前活动对象的日志。)
这些成员函数没有一个是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&);
...
};
这些构造函数实际上没有做任何事,因为这个allocator不需要初始化任何成员变量。基于同样的理由,任意两个malloc_allocator都是可互换的;如果a1和a2的类型都是malloc_allocator<int>,我们可以自由地通过a1来allocate()内存然后通过a2来deallocate()它。我们因此定义一个比较操作以表明所有的malloc_allocator对象是等价的:
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;
}
你会期望一个allocator,它的不同对象是不可替换的吗?当然--但很难提供一个简单而有用的例子。一种明显的可能性是内存池。它对大型的C程序很常见,从几个不同的位置(“池”)分配内存,而不是什么东西都直接使用malloc()。这样做有几个好处,其一是it only takes a single function call to reclaim all of the memory associated with a particular phase of the program。 使用内存池的程序可能定义诸如mempool_Alloc和mempool_Free这样的工具函数,mempol_Alloc(n, p)从池p中分配n个字节。很容易写出一个mmepool_alocator以匹配这样的架构:每个mempool_allocator对象有一个成员变量以指明它绑定在哪个池上,而mempool_allocator::allocate()将调用mempool_Alloc()从相应的池中获取内存。[注1]
最后,我们到了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 U>
malloc_allocator(const malloc_allocator<U>&) {}
template <class U>
struct rebind { typedef malloc_allocator<U> other; };
...
};
这其实意味着一个allocator类不能仅仅是单个类;它必须是一族相关类,每个都有它自己的value_type。一个allocator类必须要有一个rebind成员,因为它使得从一个类变成同族的另外一个类成为可能。
如果有一个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; };
...
};
最后,最后一个细节:对于void我们该怎么做?有时一个容器必须涉及void的指针(再一次,我们将会在下一段落研究细节),而重绑定机制几乎给了我们所需要的东西,但并不完全。它不能工作,因为我们会写出类似malloc_allocator<void>::pointer的代码,而我们定义的malloc_allocator用void实例化时是非法的。它使用了sizeof(T),和涉及了T &;当T是void时,这两个都是非法的。解决的方法如同问题本身一样简单:为void特化malloc_allocator,扔掉其它一切,只留下我们需要的void的指针。
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。
使用Allocator
使用allocator的最简单方法,当然是将它们作为参数传给容器类;用
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;
};
如果要满足对容器的需求的话(需求全集见于C++标准§23.1,Table 65),这还不是我们所要的全部样板,但是绝大部分都和allocator完全无关。对于我们的目的而言,最感兴趣的成员函数是构造函数(它分配内存和创建对象),和析构函数(它销毁内存并释放内存)。
构造函数初始化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;
}
}
}
(你可能会问为什么我们手写了这个循环;std::uninitialized_fill()不是已经做我们所需要的东西?几乎,但不完全相同。我们必须调用allocator的构造成员函数而不是简单的pacement new。也许未来的C++标准将会包含一个接受allocator为参数的uninitialized_fill()函数,从而使得不再需要这个显式循环。
析构函数比较简单,因为我们不需要担心异常安全: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);
}
}
我们这个简单的array类不需要使用重绑定或转换,但这只是因为Array<T, Allocator>绝不产生T类型以外的对象。当你定义更复杂的数据结构时,其它类型就出现了。比如,考虑一下value_type是T的链表类list。链表通常是由节点组成的,每个节点含有一个T类型的对象和一个指向下个节点的指针。于是,作为最初的尝试,我们可能定义链表的节点为:
template <class T>
struct List_node
{
T val;
List_node* next;
};
把一个新的值加入list的过程看起来可能是这样的:
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;
}
...
};
最后,以防你认为这么费劲才获得这么小的好处太不值了,提醒一下:仅仅因为你能写一个使用allocator的容器,并不意谓你必须,或你应该。有时你可能写一个依赖于特殊的内存分配策略的容器类,比如复杂到基于磁盘的B树容器或简单到我的书中所描述的block类。即使你确实想写一个使用allocator的容器类,你也不是必须支持可能的指针类型。你可以写一个容器,而要求所有用户自定义的allocator使用普通指针,并在文档中明确说明这个限制。不是每件事都要完全通用。
展望未来
如果你想写一个如同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;
}

注:
[1] You can see an example of a pool allocator in the open source SGI Pro64TM compiler, http://oss.sgi.com/projects/Pro64/.
[2] Why the funny template keyword in that expression? It's an annoying little technicality; like typename, it helps the compiler resolve a parsing ambiguity. The problem is that when A is a template parameter, and the compiler sees an expression like A::B<T>, the compiler doesn't know anything about A's members. Should it assume that B<T> is a member template, or should it assume that B is an ordinary member variable and that < is just a less than sign? A human reader knows which way to interpret it, but the compiler doesn't. You need to put in template to force the first interpretation. Formally, the rule (in §14.2 of the C++ Standard) is: "When the name of a member template specialization appears after . or -> in a postfix-expression, or after nested-name-specifier in a qualified-id, and the postfix-expression or qualified-id explicitly depends on a template-parameter (14.6.2), the member template name must be prefixed by the keyword template. Otherwise the name is assumed to name a non-template."
Most of us who use the C++ Standard library tend to forget about allocators, those mysterious things specified by default template parameters for STL containers. In most situations you will not need to call an allocator explicitly or write one of your own. But there are occasions when you might want to substitute your own custom allocator for the default version, for example, to allocate objects from a special memory pool. In this column Matt Austern discusses what you can use allocators for and how you can define your own.

Allocators are one of the most mysterious parts of the C++ Standard library. Allocators are rarely used explicitly; the Standard doesn't make it clear when they should ever be used. Today's allocators are substantially different from those in the original STL proposal, and there were two other designs in between — all of which relied on language features that, until recently, were available on few compilers. The Standard appears to make promises about allocator functionality with one hand and then take those promises away with the other.

This column will discuss what you can use allocators for and how you can define your own. I'm only going to discuss allocators as defined by the C++ Standard: bringing in pre-Standard designs, or workarounds for deficient compilers, would just add to the confusion.

When Not to Use Allocators

Allocators in the C++ Standard come in two pieces: a set of generic requirements, specified in 20.1.5 (Table 32), and the class std::allocator, specified in 20.4.1. We call a class an allocator if it conforms to the requirements of Table 32. The std::allocator class conforms to those requirements, so it is an allocator. It is the only predefined allocator class in the standard library.

Every C++ programmer already knows about dynamic memory

allocation: you write new X to allocate memory and create a new object of type X, and you write delete p to destroy the object that p points to and return its memory. You might reasonably think that allocators have something to do with new and delete — but they don't. (The Standard describes ::operator new as an "allocation function," but, confusingly, that's not the same as an allocator.)

The most important fact about allocators is that they were intended for one purpose only: encapsulating the low-level details of STL containers' memory management. You shouldn't invoke allocator member functions in your own code, unless you're writing an STL container yourself. You shouldn't try to use allocators to implement operator new[]; that's not what they're for. If you aren't sure whether you need to use allocators, then you don't.

An allocator is a class with member functions allocate and deallocate, the rough equivalents of malloc and free. It also has helper functions for manipulating the memory that it allocated and typedefs that describe how to refer to the memory — names for pointer and reference types. If an STL container allocates all of its memory through a user-provided allocator (which the predefined STL containers all do; each of them has a template parameter that defaults to std::allocator), you can control its memory management by providing your own allocator.

This flexibility is limited: a container still decides for itself how much memory it's going to ask for and how the memory will be used. You get to control which low-level functions a container calls when it asks for more memory, but you can't use allocators to make a vector act like a deque. Sometimes, though, even this limited flexibility is useful. If you have a special fast_allocator that allocates and deallocates memory quickly, for example (perhaps by giving up thread safety, or by using a small local heap), you can make the standard list class use it by writing std::list<T, fast_allocator<T> > instead of plain std::list<T>.

If this seems esoteric to you, you're right. There is no reason to use allocators in normal code.

Defining an Allocator

This already shows you something about allocators: they're templates. Allocators, like containers, have value types, and an allocator's value type must match the value type of the container it's used with. This can sometimes get ugly: map's value type is fairly complicated, so a map with an explicit allocator involves expressions like std::map<K, V, fast_allocator<std::pair<const K, V> > >. (As usual, typedefs help.)

Let's start with a simple example. According to the C++ Standard, std::allocator is built on top of ::operator new. If you're using an automatic tool to trace memory usage, it's often more convenient to have something a bit simpler than std::allocator. We can use malloc instead of ::operator new, and we can leave out the complicated performance optimizations that you'll find in a good implementation of std::allocator. We'll call this simple allocator malloc_allocator.

Since the memory management in malloc_allocator is simple, we can focus on the boilerplate that's common to all STL allocators. First, some types: an allocator is a class template, and an instance of that template allocates memory specifically for objects of some type T. We provide a series of typedefs that describe how to refer to objects of that type: value_type for T itself, and others for the various flavors of pointers and references.


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;


...


};

It's no accident that these types are so similar to those in an STL container: a container class usually gets those types directly from its allocator. Why so many typedefs? You might think that pointer is superfluous: it's just value_type*. Most of the time that's true, but you might occasionally want to define an unconventional allocator where pointer is some pointer-like class, or where it's some nonstandard vendor-specific type like value_type __far*; allocators are a standard hook for nonstandard extensions. Unusual pointer types are also the reason for the address member function, which in malloc_allocator is just an alternate spelling for operator&:


template <class T> class malloc_allocator




...{


public:




pointer address(reference x) const ...{ return &x; }




const_pointer address(const_reference x) const ...{


return &x;


}


...


};

Now we can get to the real work: allocate and deallocate. They're straightforward, but they don't look quite like malloc and free. We pass two arguments to allocate: the number of objects that we're allocating space for (max_size returns the largest request that might succeed), and, optionally, an address that can be used as a locality hint. A simple allocator like malloc_allocator makes no use of that hint, but an allocator designed for high performance might. The return value is a pointer to a block of memory that's large enough for n objects of type value_type and that has the correct alignment for that type. We also pass two arguments to deallocate: a pointer, of course, but also an element count. A container has to keep track of sizes on its own; the size arguments to allocate and deallocate must match. Again, this extra argument exists for reasons of performance, and again, malloc_allocator doesn't use it.


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);


}


...


};

The allocate and deallocate member functions deal with uninitialized memory; they don't construct or destroy objects. An expression like a.allocate(1) is more like malloc(sizeof(int)) than like new int. Before using the memory you get from allocate, you have to create some objects in that memory; before returning that memory with deallocate, you have to destroy those objects. C++ provides a mechanism for creating an object at a specific memory location: placement new. If you write new(p) T(a, b) then you are invoking T's constructor to create a new object, just as if you had written new T(a, b) or T t(a, b). The difference is that when you write new(p) T(a, b) you're specifying the location where that object is constructed: the address where p points. (Naturally, p has to point to a large enough region of memory, and it has to point to raw memory; you can't construct two different objects at the same address.) You can also call an object's destructor, without releasing any memory, by writing p->~T(). These features are rarely used, because usually memory allocation and initialization go together: it's inconvenient and dangerous to work with pointers to uninitialized memory. One of the few places where you need such low-level techniques is if you're writing a container class, so allocators decouple allocation from initialization. The member function construct performs placement new, and the member function destroy invokes the destructor.


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(); }


...


};

(Why do allocators have those member functions, when containers could use placement new directly? One reason is to hide the somewhat awkward syntax, and another is that if you're writing a more complicated allocator you might want construct and destroy to have some side effects beside object construction and destruction. An allocator might, for example, maintain a log of all currently active objects.) None of these member functions is static, so the first thing a container has to do before using an allocator is create allocator objects — and that means we should define some constructors. We don't need an assignment operator, though: once a container creates its allocator, the allocator isn't ever supposed to be changed. The allocator requirements in Table 32 don't include assignment. Just to be on the safe side, to make sure nobody uses an assignment operator accidentally, we'll disable the one that would otherwise be generated automatically.


template <class T> class malloc_allocator




...{


public:




malloc_allocator() ...{}




malloc_allocator(const malloc_allocator&) ...{}




~malloc_allocator() ...{}


private:


void operator=(const malloc_allocator&);


...


};

None of these constructors actually does anything, because this allocator doesn't have any member variables to initialize. For the same reason, any two malloc_allocator objects are interchangeable; if a1 and a2 are both of type malloc_allocator<int>, we can freely allocate memory through a1 and deallocate it through a2. We therefore define a comparison operator that says all malloc_allocator objects are equal:


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;


}

Would you ever want to have an allocator where different objects weren't interchangeable? Certainly — but simple and useful examples are hard to come by. One obvious possibility is memory pools. It's common for large C programs to allocate memory from several different places ("pools"), instead of directly doing everything through malloc. This has several benefits, one of which is that it only takes a single function call to reclaim all of the memory associated with a particular phase of the program. A program that uses memory pools might define utility functions like mempool_Alloc and mempool_Free, where mempool_Alloc(n, p) allocates n bytes from pool p. It's easy to write a mempool_allocator that fits into such a framework: each mempool_allocator object would have a member variable to specify which pool it's associated with, and mempool_allocator::allocate would invoke mempool_Alloc to get memory from the appropriate pool [1]. Finally, we get to the one tricky part of defining an allocator: mapping between different types. The problem is that an allocator class, like malloc_allocator<int>, is all built around a single value type: malloc_allocator<int>::pointer is int*, malloc_allocator<int>().allocate(1) returns enough memory for one int object, and so on. In general, however, a container class that uses malloc_allocator may have to deal with more than one type. A list class, for example, doesn't allocate ints; internally, it allocates list nodes. (We'll see more about that in the next section.) Somehow, when you create an std::list<int, malloc_allocator<int> >, the list has to turn the malloc_allocator<int> into a malloc_allocator for list nodes. The mechanism for this is called rebinding, and it has two parts. First, given an allocator type A1 whose value type is X1, you must be able to write down an allocator type A2 that's just the same as A1 except that its value type is X2. Second, given an object a1 of type A1, you must be able to create an equivalent object a2 of type A2. Both of these parts use member templates, which is why allocators were unsupported or poorly supported on older compilers.


template <class T> class malloc_allocator




...{


public:


template <class U>




malloc_allocator(const malloc_allocator<U>&) ...{}




template <class U>




struct rebind ...{ typedef malloc_allocator<U> other; };


...


};

What this really means is that an allocator class can't ever just be a single class; it has to be a family of related classes, each with its own value type. An allocator class must always have a rebind member, because that's what makes it possible to go from one class in that family to another. If you have an allocator class A1, the corresponding allocator class for a different value type is typename A1::template rebind<X2>::other [2]. And just as you can convert from one type to another, the templatized converting constructor lets you convert values: you can write malloc_allocator<int>(a) whether a is an object of type malloc_allocator<int>, or malloc_allocator<double>, or malloc_allocator<string>. As usual, malloc_allocator's converting constructor doesn't do anything because malloc_allocator has no member variables. Incidentally, while most allocators have a single template parameter (the allocator's value type) that's not a requirement — it just often happens to be convenient. The rebind mechanism would work just as well for allocators with multiple template parameters:


template <class T, int flags> class my_allocator




...{


public:


template <class U>




struct rebind ...{ typedef my_allocator<U, flags> other; };


...


};

Finally, one last detail: what do we do about void? Sometimes a container has to refer to void pointers (again, we'll see more about that in the next section), and the rebind mechanism almost gives us what we need, but not quite. It doesn't work, because we would need to write something like malloc_allocator<void>::pointer, and we've defined malloc_allocator in such a way that instantiating it for void would be illegal. It uses sizeof(T), and it refers to T&; neither is legal when T is void. The solution is as simple as the problem: specialize malloc_allocator for void, leaving out everything except the bare minimum that we need for referring to void pointers.


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; };

That's it! The complete source code for malloc_allocator is shown in Listing 1.

Using Allocators

The easiest way to use allocators, of course, is to pass them as arguments to container classes; write

std::vector<char, malloc_allocator<char> > V;

instead of plain std::vector<char>, or


typedef std::list<int, mempool_allocator<int> > List;


List L(mempool_allocator<int>(p));

instead of plain std::list<int>. But you can do more than that. The whole point of the STL is that it's extensible: just as you can write your own allocators, you can also write your own container classes. If you are careful, and if you write a container class that uses its allocator for all memory-related functionality, then somebody else will be able to plug in their own custom-written allocators. Container classes like std::vector and std::list are complicated, and a lot of the complexity has nothing to do with memory management. Let's start with a simple example, so that we can focus just on the allocators. Consider a fixed-size array class, Array, where the number of elements is set in the constructor and can never change thereafter. (This is something like a simplified version of std::valarray.) We'll have two template parameters, the element type and an allocator type. Containers, like allocators, start with nested types: value_type, reference, const_reference, size_type, difference_type, iterator, and const_iterator. In general, most of those types can be taken directly from the container's allocator — thus illustrating why the container's value type and the allocator's must match. The iterator types, of course, don't usually come from the allocator; usually an iterator is some kind of class, closely tied to the container's internal representation. The Array class is simpler than usual because it's natural to store all of our elements in a single contiguous memory block; we'll maintain a pointer to the beginning of the block and a pointer to the end. The iterators will just be pointers. Before we go any further, we have to make a decision: how are we going to store the allocator? The constructor will take an allocator object as one of its arguments. We need to hold on to a copy of the allocator throughout the lifetime of the container, since we'll still need it in the destructor. In some sense, there's no problem here: we could just declare a member variable of type Allocator and be done with it. That solution would be correct, but annoying. Ninety-nine percent of the time, after all, users don't want to bother thinking about allocators; they'll just write Array<int> and use the default — and the default allocator is probably an empty class with no non-static member variables. The trouble is that a member variable of type Allocator will take up even when Allocator is an empty class. (This is required by the C++ Standard.) Our Array class will have three words of overhead, instead of two. Maybe an extra word of overhead isn't a big deal, but it's annoying to burden all users with that overhead for a feature that most of them will never use. There are a number of solutions to this problem, some of which rely on traits classes and partial specialization. Probably the simplest solution is to use a (private) base class of type Allocator instead of a member variable. Compilers are allowed to optimize away empty base classes, and nowadays most compilers do. We can finally write down a skeleton class definition:


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;


};




This doesn't yet have all of the boilerplate that we need if we are to satisfy the container requirements (see Table 65, in 23.1 of the C++ Standard, for the complete set of requirements), but most of that boilerplate has little to do with allocators. For our purposes, the most interesting member functions are the constructor, which allocates memory and creates objects, and the destructor, which destroys memory and frees memory. The constructor initializes the allocator base class, obtains a block of memory that's large enough for n elements (if we were writing something like vector, we might obtain a larger block to allow room for growth), and then loops through the block creating copies of the initial value. The only tricky part is exception safety: if one of the elements' constructors throws an exception, we have to undo everything we've done.




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;


}


}


}



(You might wonder why we're writing out these loops by hand; doesn't std::uninitialized_fill already do what we need? Almost, but not quite. We have to use the allocator's construct member function instead of plain placement new. Perhaps a future version of the C++ Standard will include a version of uninitialized_fill that takes an allocator argument and make such explicit loops unnecessary.) The destructor is simpler, since we don't have to worry about exception safety: T's destructor is never supposed to throw.


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);


}


}

Our simple array class doesn't have to use rebinding or conversion, but that's only because Array<T, Allocator> never creates objects of any type other than T. Other types come in when you define more complicated data structures. Consider, for example, a linked list class whose value type is T. A linked list typically consists of nodes, where each node holds an object of type T and a pointer to the next node. So, as a first attempt, we might define a list node as follows:


template <class T>


struct List_node




...{


T val;


List_node* next;


};

The procedure for adding a new value to the list might look something like this:

Using an allocator whose value type is List_node<T>, allocate memory for a new list node.

Using an allocator whose value type is T, construct the new list element in the node's val slot.

Link the node into place appropriately.

This procedure requires dealing with two different allocators, one of which is obtained from the other via rebinding. It's good enough for almost all applications, even ones that use allocators for quite sophisticated purposes. What it doesn't do is support allocators with unusual pointer types. It explicitly relies on being able to use an ordinary pointer of type List_node<T>*. If you're extremely ambitious, and you want to support allocators with alternative pointer types, everything suddenly becomes much more complicated — the pointer from one list node to another can no longer be of type List_node<T>*, or even of type void*, but must be of some type taken from the allocator. Writing this without circularity is nontrivial: it's illegal to instantiate an allocator with an incomplete type, so there's no way to talk about pointers to List_node until after List_node has been fully defined. We need a delicate chain of declarations.


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;


}




...


};

And finally, in case you think that this is far too much effort for far too small a benefit, a reminder: just because you can write a container class that uses allocators doesn't mean that you have to, or that you should. Sometimes you might want to write a container class that relies on a specific allocation strategy, whether it's something as ambitious as a disk-based B-tree container or as simple as the block class that I describe in my book. Even if you do want to write a container class that uses allocators, you don't have to support alternate pointer types. You can write a container where you require that any user-supplied allocator uses ordinary pointers, and document that restriction. Not everything has to be fully general.

Future Directions

If you want to write a simple allocator like malloc_allocator, you should have no difficulty. (Provided that you're using a reasonably modern compiler, that is.) If you have more ambitious plans, however — a memory pool allocator or an allocator with nonstandard pointer types for distributed computing — the situation is less satisfactory.

If you want to use some alternative pointer-like type, what operations does it have to support? Must it have a special null value, and, if so, how is that value written? Can you use casts? How can you convert between pointer-like objects and ordinary pointers? Do you have to worry about pointer operations throwing exceptions? I made some assumptions in the last section; the C++ Standard doesn't say whether those assumptions are right or wrong. These details are left to individual standard library implementations, and it's even legal for an implementation to ignore alternative pointer types altogether. The C++ Standard also leaves a few unanswered questions about what happens when different instances of an allocator aren't interchangeable.

Fortunately, the situation isn't quite as dire as the words in the Standard (20.1.5, paragraphs 4-5) might make it seem. The Standard left some questions unanswered because, at the time it was written, the C++ standardization committee wasn't able to agree on the answers; the necessary experience with allocators did not exist. Everyone involved in writing this section of the Standard considered it to be a temporary patch, and the vagueness will definitely be removed in a future revision.

For the moment, it's best to stay away from alternative pointer types if you're concerned about portability, but, if you're willing to accept a few limitations, you can safely use allocators like mempool_allocator where the differences between individual objects is important. All major standard library implementations now support such allocators in some way, and the differences between implementations are minor.

Just as the containers take allocator types as template parameters, so the containers' constructors take allocator objects as arguments. A container makes a copy of that argument and uses the copy for all of its memory management; once it is initialized in the constructor, the container's allocator is never changed.

The only question is what happens when you perform an operation that requires two containers to cooperate on memory management. There are exactly two such operations in the standard library: swap (all containers) and std::list::splice. In principle, an implementation could handle them in several different ways:

Forbid swap or splice between two containers whose allocators aren't equal.

Put a test for allocator equality in swap and splice. If the allocators aren't equal, then fall back to some other operation like copying and assignment.

For swap only: swap the containers' allocators as well as their data. (It's hard to see how we could generalize this to splice. It also presents a problem: how do you swap things that don't have assignment operators?)

If you just stay away from swap and splice whenever two containers might be using different allocators, you'll be safe. In practice, I haven't found this to be a serious restriction: you need tight discipline to use a feature like memory pools safely, and you probably won't want indiscriminate mixing between containers with different allocators.

Partly because of unfamiliarity and partly because of the unsatisfactory state of the C++ Standard's requirements, most uses of allocators today are simple. As the C++ community becomes more familiar with allocators, and as the Standard is clarified, we can expect more sophisticated uses to emerge.

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;


}

Notes

[1] You can see an example of a pool allocator in the open source SGI Pro64TM compiler, http://oss.sgi.com/projects/Pro64/.
[2] Why the funny template keyword in that expression? It's an annoying little technicality; like typename, it helps the compiler resolve a parsing ambiguity. The problem is that when A is a template parameter, and the compiler sees an expression like A::B<T>, the compiler doesn't know anything about A's members. Should it assume that B<T> is a member template, or should it assume that B is an ordinary member variable and that < is just a less than sign? A human reader knows which way to interpret it, but the compiler doesn't. You need to put in template to force the first interpretation. Formally, the rule (in 14.2 of the C++ Standard) is: "When the name of a member template specialization appears after . or -> in a postfix-expression, or after nested-name-specifier in a qualified-id, and the postfix-expression or qualified-id explicitly depends on a template-parameter (14.6.2), the member template name must be prefixed by the keyword template. Otherwise the name is assumed to name a non-template."
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: