详解STL中的allocator

实例化的时候不需要自己定义allocator。如果你要自己编写allocator的话 
1:没有必要 2.对于新手来说很复杂。 




所以你直接用默认的就可以了。默认的allocator是一个模板,会自动的帮你替换成allocator的,所以你不需要管。 


想用字符串的向量直接:std::vector就可以了。




条款10:注意分配器的协定和约束
分配器是怪异的。它们最初是为抽象内存模型而开发的,允许库开发者忽略在某些16位操作系统上near和far指针的区别(即,DOS和它的有害产物),但努力失败了。分配器也被设计成促进全功能内存管理器的发展,但事实表明那种方法在STL的一些部分会导致效率损失。为了避免效率冲击,C++标准委员会向标准中添加了词语,把分配器弱化为对象,同时也表达了他们不会让操作损失能力的希望。


还有更多。正如operator new和operator new[],STL分配器负责分配(和回收)原始内存,但分配器的客户接口与operator new、operator new[]甚至malloc几乎没有相似之处。最后(而且可能非常惊人),大多数标准容器从未向它们相关的分配器索要内存。从没有。结果分配器是,嗯,分配器是怪异的。


当然,那不是它们的错,而且无论如何,这不意味着它们没用。但是,在我解释分配器好在哪里之前(那是条款11的主题),我需要解释它们哪里不好。有许多事情分配器好像能做,但不能,而且在你试图开始使用之前,知道领域的边界很重要。如果不,你将肯定会受伤。此外,关于分配器的事实如此独特,总结它的行为既有启发性又有趣。至少我希望是。


分配器的约束的列表从用于指针和引用的残留typedef开始。正如我提到的,分配器最初被设想为抽象内存模型,在那种情况下,分配器在它们定义的内存模型中提供指针和引用的typedef才有意义。在C++标准里,类型T的对象的默认分配器(巧妙地称为allocator)提供typedef allocator::pointer和allocator::reference,而且也希望用户定义的分配器也提供这些typedef。


C++老手立即发现这有问题,因为在C++里没有办法捏造引用。这样做要求有能力重载operator.(“点操作符”),而那是不允许的。另外,建立行为像引用的对象是使用代理对象的例子,而代理对象会导致很多问题。(一个这样的问题产生了条款18。对代理对象的综合讨论,转向《More Effective C++》的条款30,你能知道什么时候它们工作什么时候不。)


就STL里的分配器而言,没有任何代理对象的技术缺点会导致指针和引用typedef失效,实际上标准明确地允许库实现假设每个分配器的pointer typedef是T*的同义词,每个分配器的reference typedef与T&相同。对,库实现可以忽视typedef并直接使用原始指针和引用!所以即使你可以设法写出成功地提供新指针和引用类型的分配器的方法,也好不到哪里去,因为你使用的STL实现将自由地忽视你的typedef。很优雅,不是吗?


当你钦佩标准化的怪癖时,我将再介绍一个。分配器是对象,那表明它们可能有成员功能,内嵌的类型和typedef(例如pointer和reference)等等,但标准允许STL实现认为所有相同类型的分配器对象都是等价的而且比较起来总是相等。很唐突,听起来并不可怕,而且对它当然有好的动机。考虑这段代码:


template    // 一个用户定义的分配器
class SpecialAllocator {...};   // 模板
typedef SpecialAllocator SAW;  // SAW = “SpecialAllocator
      // for Widgets”
list L1;
list L2;
...
L1.splice(L1.begin(), L2);   // 把L2的节点移到
      // L1前端
记住当list元素从一个list被接合到另一个时,没有拷贝什么。取而代之的是,调整了一些指针,曾经在一个list中的节点发现他们自己现在在另一个list中。这使接合操作既迅速又异常安全。在上面的例子里,接合前在L2里的节点接合后出现在L1中。


当L1被销毁时,当然,它必须销毁它的所有节点(以及回收它们的内存),而因为它现在包含最初是L2一部分的节点,L1的分配器必须回收最初由L2的分配器分配的节点。现在清楚为什么标准允许STL实现认为相同类型的分配器等价。所以由一个分配器对象(比如L2)分配的内存可以安全地被另一个分配器对象(比如L1)回收。如果没有这样的认为,接合操作将更难实现。显然它们不能像现在一样高效。(接合操作的存在也影响了STL的其他部分。另一个例子参见条款4。)


那当然好,但你想得越多,越会意识到STL实现可以认为相同类型的分配器等价是多严厉的约束。那意味着可移植的分配器对象——在不同的STL实现下都功能正确的分配器——不能有状态。让我们明确这一点:它意味着可移植的分配器不能有任何非静态数据成员,至少没有会影响它们行为的。一个都没有。没有。那表示,例如,你不能有从一个堆分配的SpecialAllocator和从另一个堆分配的另一个SpecialAllocator。这样的分配器不等价,而试图使用那两个分配器的现存STL实现可能导致错误的运行期数据结构。


注意这是一个运行期问题。有状态的分配器可以很好地编译。它们只是不按你期待的方式运行。确保一个给定类型的所有分配器都等价是你的责任。如果你违反这个限制,不要期待编译器发出警告。


为了对标准委员会公平,我应该指出,在“允许STL实现认为相同类型的分配器等价”的文字之后,紧接着有下列陈述:


鼓励实现提供...支持非相等实例的库。在那样的实现中,...当分配器实例非相等时,容器和算法的语义是由实现定义的。


这是个可爱的句子,但是作为一个考虑开发带状态自定义分配器的STL用户,它几乎没向你提供什么。你可以利用这句话除非(1)你知道你使用的STL实现支持不等价的分配器,(2)你愿意钻研它们的文档来确定你是否可以接受“非相等”分配器的实现定义行为,(3)你不关心把你的代码移植到那些可能从标准给予的自由中获得好处的STL实现。简而言之,这个段落——第20.1.5节第5段,为坚持要求知道的那些人——是标准为分配器的“I have a dream”演讲。在梦想成为现实之前,关心移植性的程序员应该把他们自己限制在没有状态的自定义分配器。


我早先提及了分配器在分配原始内存方面类似operator new,但它们的接口不同。如果你看看operator new和allocator::allocate最普通形式的声明,就会很清楚:


void* operator new(size_t bytes);
pointer allocator::allocate(size_type numObjects);
     // 记住事实上“pointer”总是
     // T*的typedef
两者都带有一个指定要分配多少内存的参数,但对于operator new,这个参数指定的是字节数,而对于allocator::allocate,它指定的是内存里要能容纳多少个T对象。例如,在sizeof(int) == 4的平台上,如果你要足够容纳一个int的内存,你得把4传给operator new,但你得把1传给allocator::allocate。(在operator new情况下这个参数的类型是size_t,而在allocate的情况下它是allocator::size_type。在两种情况里,它都是无符号整数类型,通常allocator::size_type是一个size_t的typedef。)关于这个差异没有什么“错误”,但是operator new和allocator::allocate之间的不同协定使应用自定义operator new的经验到开发自定义分配器的过程变得复杂。


operator new和allocator::allocate的返回类型也不同。operator new返回void*,那是C++传统的表示一个到未初始化内存的指针的方式。allocator::allocate返回一个T*(通过pointer typedef),不仅不传统,而且是有预谋的欺诈。从allocator::allocate返回的指针并不指向一个T对象,因为T还没有被构造!在STL里暗示的是希望allocator::allocate的调用者将最后在它返回的内存里构造一个或多个T对象(也许通过allocator::construct,通过uninitialized_fill或通过raw_storage_iterator的一些应用),虽然在这里没有发生vector::reserve或string::reserve(参见条款14)。在operator new和allocator::allocate之间返回类型的不同使未初始化内存的概念模型发生了变化,而它再次使把关于实现operator new的知识应用到开发自定义分配器变得困难。


那也带来了我们对STL分配器最后的好奇——大多数标准容器从未调用它们例示的分配器。这是两个例子:


list L;    // 和list >一样;
     // allocator从未用来
     // 分配内存!
set s;   // 记住SAW是一个
     // SpecialAllocator的typedef;
     // SAW从未分配内存!这个怪癖对list和所有标准关联容器都是真的(set、multiset、map和multimap)。那是因为这些是基于节点的容器,即,这些容器所基于的数据结构是每当值被储存就动态分配一个新节点。对于list,节点是列表节点。对于标准关联容器,节点通常是树节点,因为标准关联容器通常用平衡二叉搜索树实现。


想一会儿可能怎么实现list。list本身由节点组成,每个节点容纳一个T对象和到list中后一个和前一个节点的指针:


template typename Allocator = allocator > // 实现
class list{
private:
 Allocator alloc;  // 用于T类型对象的分配器


 struct ListNode{  // 链表里的节点
  T data:
  ListNode *prev;
  ListNode *next;
 };
 ...
};
当添加一个新节点到list时,我们需要从分配器为它获取内存,我们要的不是T的内存,我们要的是包含了一个T的ListNode的内存。那使我们的Allocator对象没用了,因为它不为ListNode分配内存,它为T分配内存。现在你理解list为什么从未让它的Allocator做任何分配了:分配器不能提供list需要的。


list需要的是从它的分配器类型那里获得用于ListNode的对应分配器的方法。按照协定,分配器得提供完成那个工作的typedef,否则将会很难办。那个typedef叫做other,但它不那么简单,因为other是嵌入一个叫做rebind的结构体的typedef,rebind自己是一个嵌入分配器的模板——分配器本身也是模板!


请不要试图考虑最后那句话。取而代之的是,看看下段代码,然后直接阅读后面的解释。


template   // 标准分配器像这样声明,
class allocator {   // 但也可以是用户写的
public:     // 分配器模板
 template
 struct rebind{
  typedef allocator other;
 }
 ...
};
在list的实现代码里,需要确定我们持有的T的分配器所对应的ListNode的分配器类型。我们持有的T的分配器类型是模板参数Allocator。在本例中,ListNodes的对应分配器类型是:


Allocator::rebind::other和我保持一致。每个分配器模板A(例如,std::allocator,SpecialAllocator,等)都被认为有一个叫做rebind的内嵌结构体模板。rebind带有一个类型参数,U,并且只定义一个typedef,other。 other是A的一个简单名字。结果,list可以通过Allocator::rebind::other从它用于T对象的分配器(叫做Allocator)获取对应的ListNode对象分配器。


这或许对你有意义,或许不。(如果你注视它足够长时间了,它会,但你可能还必须注视一会儿。我知道我必须。)作为一个可能想要写自定义分配器的STL用户,你其实不需要知道它怎样工作。你需要知道的是如果你选择写分配器并让标准容器使用它们,你的分配器必须提供rebind模板,因为标准容器认为它在那里。(为了调试的目的,知道T对象的基于节点的容器为什么从未从T对象的分配器获取内存也是有帮助的。)


哈利路亚!我们最后完成了对分配器特质的检查。因此,如果你想要写自定义分配器,让我们总结你需要记得的事情。


把你的分配器做成一个模板,带有模板参数T,代表你要分配内存的对象类型。 
提供pointer和reference的typedef,但是总是让pointer是T*,reference是T&。 
决不要给你的分配器每对象状态。通常,分配器不能有非静态的数据成员。 
记得应该传给分配器的allocate成员函数需要分配的对象个数而不是字节数。也应该记得这些函数返回T*指针(通过pointer typedef),即使还没有T对象被构造。 
一定要提供标准容器依赖的内嵌rebind模板。 
写你自己的分配器时你必须做的大部分事情是重现大量样板代码,然后修补一些成员函数,特别是allocate和deallocate。我建议你从Josuttis的样例allocator网页[23]或Austern的文章《What Are Allocators Good For?》[24]的代码开始,而不是从头开始写样板。


一旦你消化了本条款中的信息,你将知道很多关于分配器不能做的事情,但是那或许不是你想要知道的。相反,你或许想知道分配器能做什么。那有权成为一个丰富的主题,一个我称为“条款11”的主题。






条款11:理解自定义分配器的正确用法
你用了基准测试,性能剖析,而且实验了你的方法得到默认的STL内存管理器(即allocator )在你的STL需求中太慢、浪费内存或造成过度的碎片的结论,并且你肯定你自己能做得比它好。或者你发现allocator对线程安全采取了措拖,但是你只对单线程的程序感兴趣,你不想花费你不需要的同步开销。或者你知道在某些容器里的对象通常一同被使用,所以你想在一个特别的堆里把它们放得很近使引用的区域性最大化。或者你想建立一个相当共享内存的唯一的堆,然后把一个或多个容器放在那块内存里,因为这样它们可以被其他进程共享。 恭喜你!这些情况正好对应于一种适合于自定义分配器解决的方案。


例如,假定你有仿效malloc和free的特别程序,用于管理共享内存的堆,


void* mallocShared(size_t bytesNeeded);
void freeShared(void *ptr); 并且你希望能把STL容器的内容放在共享内存中。没问题:


template
class SharedMemoryANocator {
public:
 ...
 pointer allocate(size_type numObiects, const void *localityHint = 0)
 {
  return static_cast(mallocShared(numObiects * sizeof(T)));
 }


 void deallocate(pointer ptrToMemory, size_ type numObjects)
 {
  freeShared(ptrToMiemory);
 } 
 ... 
};allocate里的pointer类型、映射和乘法的更多信息参见条款10。


你可以像这样使用SharedMemoryAllocator:


// 方便的typedef
typedef vector >
   SharedDoubleVec;
...
{       // 开始一个块
 SharedDoubleVec v;     // 建立一个元素在
       // 共享内存中的vector
 ...      // 结束这个块
}
在紧挨着v定义的注释里的词语很重要。v使用SharedMemoryAllocator,所以v分配来容纳它元素的内存将来自共享内存,但v本身——包括它的全部数据成员——几乎将肯定不被放在共享内存里,v只是一个普通的基于堆的对象,所以它将被放在运行时系统为所有普通的基于堆的对象使用的任何内存。那几乎不会是共享内存。为了把v的内容和v本身放进共享内存,你必须做像这样的事情:


void *pVectorMemory =     // 分配足够的共享内存
  mallocShared(sizeof(SharedDoubleVec)); // 来容纳一个
       // SharedDoubleVec对象
SharedDoubleVec *pv =     // 使用“placement new”来
  new (pVectorMemory) SharedDoubleVec;  // 在那块内存中建立
       // 一个SharedDoubleVec对象;
       // 参见下面
       // 这个对象的使用(通过pv)
...
pv->~SharedDoubleVec();     // 销毁共享内存
       // 中的对象
freeShared(pVectorMemory);     // 销毁原来的
       // 共享内存块我希望那些注释让你清楚是怎么工作的。基本上,你获得一些共享内存,然后在里面建立一个用共享内存为自己内部分配的vector。当你用完这个vector时,你调用它的析构函数,然后释放vector占用的内存。代码不很复杂,但我们在上面所做的比仅仅声明一个本地变量要苛刻得多。除非你真的要让一个容器(与它的元素相反)在共享内存里,否则我希望你能避免这个手工的四步分配/建造/销毁/回收的过程。


在这个例子里,无疑你已经注意到代码忽略了mallocShared可能返回一个null指针。显而易见,产品代码必须考虑这样一种可能性。 此外,共享内存中的vector的建立由“placement new”完成。如果你不熟悉placement new,你最喜欢C++课本应该可以告诉你。如果那个课本碰巧是《More Effective C++》,你将发现这个玩笑在条款8兑现。


作为分配器作用的第二个例子,假设你有两个堆,命名为Heap1和Heap2类。每个堆类有用于进行分配和回收的静态成员函数:


class Heap1 {
public:
 ...
 static void* alloc(size_t numBytes, const void *memoryBlockToBeNear);
 static void dealloc(void *ptr);
 ...
};


class Heap2 { ... };  // 有相同的alloc/dealloc接口
更进一步认为你想在不同的堆里联合定位一些STL容器的内容。同样没有问题。首先,你设计一个分配器,使用像Heap1和Heap2那样用于真实内存管理的类:


template
class SpecificHeapAllocator {
public:
 pointer allocate(size_type numObjects, const void *localityHint = 0)
 {
  return static_cast(Heap::alloc(numObjects * sizeof(T), 
       localityHint));
 }


 void deallocate(pointer ptrToMemory, size_type numObjects)
 {
  Heap::dealloc(ptrToMemory);
 }
 ...
};
然后你使用SpecificHeapAllocator来把容器的元素集合在一起:


vector > v;   // 把v和s的元素
set > s;   // 放进Heap1


list   SpecificHeapAllocator > L;  // 把L和m的元素
map,      // 放进Heap2
  SpecificHeapAllocator,
     Heap2> > m;
在这个例子里,很重要的一点是Heap1和Heap2是类型而不是对象。STL为用不同的分配器对象初始化相同类型的不同STL容器提供了语法,但是我将不让你看它是什么。那是因为如果Heap1和Heap2是对象而不是类型,那么它们将是不等价的分配器,那就违反了分配器的等价约束,在条款10有详细说明。


因为这些例子演示的,分配器在许多情况里有用。只要你遵循相同类型的所有分配器都一定等价的限制条件,你将毫不费力地使用自定义分配器来控制一般内存管理策略,群集关系和使用共享内存以及其他特殊的堆。

参考资料:详情请参看Effective STL 


你可能感兴趣的:(C++)