STL Allocator

C++内存分配详解四:std::alloc行为剖析 C++标准容器分配器的动作详细分析 阅读详情

Allocator是C++语言标准库中最神秘的部分之一。它们很少被显式使用,标准也没有明确出它们应该在什么时候被使用。今天的allocator与最初的STL建议非常不同,在此过程中还存在着另外两个设计--这两个都依赖于语言的一些特性,而直到最近才在很少的几个编译器上可用。对 allocator的功能,标准似乎在一些方面追加了承诺,而在另外一些方面撤销了承诺。

这篇专栏文章将讨论你能用allocator来做什么以及如何定义一个自己的版本。我只会讨论C++标准所定义的allocator:引入准标准时代的设计,或试图绕过有缺陷的编译器,只会增加混乱。

1 什么时候不使用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的。

2 定义一个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本身,其它的则是有各种修饰字的指针和引用。
  1. template <class T> class malloc_allocator
  2. {
  3. public:
  4.   typedef T                 value_type;
  5.   typedef value_type*       pointer;
  6.   typedef const value_type* const_pointer;
  7.   typedef value_type&       reference;
  8.   typedef const value_type& const_reference;
  9.   typedef std::size_t       size_type;
  10.   typedef std::ptrdiff_t    difference_type;
  11.   ...
  12. };
复制代码
这些类型与STL容器中的很相似,这不是巧合:容器类常常直接从它的allocator提取这些类型。

为什么有这么多的typedef?你可能认为pointer是多余的:它就是value_type *。绝大部份时候这是对的,但你可能有时候想定义非传统的allocator,它的pointer是一个pointer-like的class,或非标的厂商特定类型value_type __far *;allocator是为非标扩展提供的标准hook。不寻常的pointer类型也是存在address()成员函数的理由,它在 malloc_allocator中只是operator &()的另外一种写法:
  1. template <class T> class malloc_allocator
  2. {
  3. public:
  4.   pointer address(reference x) const { return &x; }
  5.   const_pointer address(const_reference x) const {
  6.     return &x;
  7.   }
  8.   ...

  9. };
复制代码
现在我们能开始真正的工作:allocate()和deallocate()。它们很简单,但并不十分象malloc()和free()。我们传给 allocate()两个参数:我们正在为其分派空间的对象的数目(max_size返回可能成功的最大请求值),以及可选的一个地址值(可以被用作一个位置提示)。象malloc_allocator这样的简单的allocator没有利用那个提示,但为高性能而设计的allocator可能会利用它。返回值是一个指向内存块的指针,它足以容纳n个value_type类型的对象并有正确的对齐。

我们也传给deallocate()两个参数:当然一个是指针,但同样还有一个元素计数值。容器必须自己掌握大小信息;传给allocate和 deallocate的大小参数必须匹配。同样,这个额外的参数是为效率而存在的,而同样,malloc_allocator不使用它。
  1. template <class T> class malloc_allocator
  2. {
  3. public:
  4.   pointer allocate(size_type n, const_pointer = 0) {
  5.     void* p = std::malloc(n * sizeof(T));
  6.     if (!p)
  7.       throw std::bad_alloc();
  8.     return static_cast<pointer>(p);
  9.   }
  10.   void deallocate(pointer p, size_type) {
  11.     std::free(p);
  12.   }
  13.   size_type max_size() const {
  14.     return static_cast<size_type>(-1) / sizeof(value_type);
  15.   }
  16.   ...
  17. };
复制代码
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()调用析构函数。
  1. template <class T> class malloc_allocator
  2. {
  3. public:
  4.   void construct(pointer p, const value_type& x) {
  5.     new(p) value_type(x);
  6.   }
  7.   void destroy(pointer p) { p->~value_type(); }
  8.   ...
  9. };
复制代码
(为什么allocator有那些成员函数,什么时候容器可以直接使用placement new?一个理由是要隐藏笨拙的语法,而另一个是如果写一个更复杂的allocator时你可能想在构造和销毁对象时construct()和 destroy()还有其它一些副作用。比如,allocator可能维护一个所有当前活动对象的日志。)

这些成员函数没有一个是static的,因此,容器在使用allocator前做的第一件事就是创建一个allocator对象--也就是说我们应该定义一些构造函数。但是,我们不需要赋值运算:一旦容器创建了它的allocator,这个allocator就从没想过会被改变。表32中的对 allocator的需求没有包括赋值。只是基于安全,为了确保没人偶然使用了赋值运算,我们将禁止掉这个可能自动生成的函数。
  1. template <class T> class malloc_allocator
  2. {
  3. public:
  4.   malloc_allocator() {}
  5.   malloc_allocator(const malloc_allocator&) {}
  6.   ~malloc_allocator() {}
  7. private:
  8.   void operator=(const malloc_allocator&);
  9.   ...
  10. };
复制代码
这些构造函数实际上没有做任何事,因为这个allocator不需要初始化任何成员变量。基于同样的理由,任意两个malloc_allocator都是可互换的;如果a1和a2的类型都是malloc_allocator<int>,我们可以自由地通过a1来allocate()内存然后通过 a2来deallocate()它。我们因此定义一个比较操作以表明所有的malloc_allocator对象是等价的:
  1. template <class T>
  2. inline bool operator==(const malloc_allocator<T>&,
  3.                        const malloc_allocator<T>&) {
  4.   return true;
  5. }
  6. tmplate <class T>
  7. inline bool operator!=(const malloc_allocator<T>&,
  8.                        const malloc_allocator<T>&) {
  9.   return false;
  10. }
复制代码
你会期望一个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不能被老的编译器支持,或支持得很差的原因。
  1. template <class T> class malloc_allocator
  2. {
  3. public:
  4.   template <class X>
  5.   malloc_allocator(const malloc_allocator<X>&) {}
  6.   template <class X>
  7.   struct rebind { typedef malloc_allocator<X> other; };
  8.   ...
  9. };
复制代码
这其实意味着一个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上也工作良好:
  1. template <class T, int flags> class my_allocator
  2. {
  3. public:
  4.   template <class U>
  5.   struct rebind { typedef my_allocator<U, flags> other; };
  6.   ...
  7. };
复制代码
最后,最后一个细节:对于void我们该怎么做?有时一个容器必须涉及void的指针(再一次,我们将会在下一段落研究细节),而重绑定机制几乎给了我们所需要的东西,但并不完全。它不能工作,因为我们会写出类似malloc_allocator<void>::pointer的代码,而我们定义的malloc_allocator用void实例化时是非法的。它使用了sizeof(T),和涉及了T &;当T是void时,这两个都是非法的。解决的方法如同问题本身一样简单:为void特化malloc_allocator,扔掉其它一切,只留下我们需要的void的指针。
  1. template<> class malloc_allocator<void>
  2. {
  3.   typedef void        value_type;
  4.   typedef void*       pointer;
  5.   typedef const void* const_pointer;
  6. template <class U>
  7. struct rebind { typedef malloc_allocator<U> other; };
  8. };
复制代码
就是它了!malloc_allocator的完备源码见Listing 1。

3 使用Allocator

使用allocator的最简单方法,当然是将它们作为参数传给容器类;用
  1. std::vector<char, malloc_allocator<char> > V;
复制代码
取代简单的std::vector<char>,或用
  1. typedef std::list<int, mempool_allocator<int> > List;
  2. 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类和偏特化。或许最简单的解决方法就是使用(私有)继承而不是成员变量。编译器被允许优化掉空基类,而且时下绝大多数的编译器都这么做了。

我们最终能写下定义的骨架了:
  1. template <class T, class Allocator = std::allocator<T> >
  2. class Array : private Allocator
  3. {
  4. public:
  5.   typedef T value_type;
  6.   typedef typename Allocator::reference reference;
  7.   typedef typename Allocator::const_reference const_reference;
  8.   typedef typename Allocator::size_type size_type;
  9.   typedef typename Allocator::difference_type difference_type;
  10.   typedef typename Allocator::pointer       iterator;
  11.   typedef typename Allocator::const_pointer const_iterator;
  12.   typedef Allocator allocator_type;

  13.   allocator_type get_allocator() const {
  14.     return static_cast<const Allocator&>(*this);
  15.   }

  16.   iterator begin()             { return first; }
  17.   iterator end()               { return last; }
  18.   const_iterator begin() const { return first; }
  19.   const_iterator end() const   { return last; }
  20.   Array(size_type n = 0,
  21.         const T& x = T(),
  22.         const Allocator& a = Allocator());
  23.   Array(const Array&);

  24.   ~Array();

  25.   Array& operator=(const Array&);

  26. private:
  27.   typename Allocator::pointer first;
  28.   typename Allocator::pointer last;
  29. };
复制代码
如果要满足对容器的需求的话(需求全集见于C++标准§23.1,Table 65),这还不是我们所要的全部样板,但是绝大部分都和allocator完全无关。对于我们的目的而言,最感兴趣的成员函数是构造函数(它分配内存和创建对象),和析构函数(它销毁内存并释放内存)。

构造函数初始化allocator基类,获得足够容纳n个元素的一块内存(如果我们正在写类似于vector的东西,我们可能申请更大些的内存以供增长用),然后遍历内存以创建初始化值的拷贝。唯一的问题是异常安全:如果某一个元素的构造函数抛出了一个异常,我们必须撤销我们所做的一切。
  1. template <class T, class Allocator>
  2. Array<T, Allocator>::Array(size_type n,
  3.                            const T& x,
  4.                            const Allocator& a)
  5.   : Allocator(a), first(0), last(0)
  6. {
  7.   if (n != 0) {
  8.     first = Allocator::allocate(n);
  9.     size_type i;
  10.     try {
  11.       for (i = 0; i < n; ++i) {
  12.         Allocator::construct(first + i, x);
  13.       }
  14.     }
  15.     catch(...) {
  16.       for(size_type j = 0; j < i; ++j) {
  17.         Allocator::destroy(first + j);
  18.       }
  19.       Allocator::deallocate(first, n);
  20.       throw;
  21.     }
  22.   }
  23. }
复制代码
(你可能会问为什么我们手写了这个循环;std::uninitialized_fill()不是已经做我们所需要的东西?几乎,但不完全相同。我们必须调用 allocator的构造成员函数而不是简单的pacement new。也许未来的C++标准将会包含一个接受allocator为参数的uninitialized_fill()函数,从而使得不再需要这个显式循环。

析构函数比较简单,因为我们不需要担心异常安全:T的析构函数被假设为从不抛出异常。
  1. template <class T, class Allocator>
  2. Array<T, Allocator>::~Array()
  3. {
  4.   if (first != last) {
  5.     for (iterator i = first; i < last; ++i)
  6.       Allocator::destroy(i);
  7.     Allocator::deallocate(first, last - first);
  8.   }
  9. }
复制代码
我们这个简单的array类不需要使用重绑定或转换,但这只是因为Array<T, Allocator>绝不产生T类型以外的对象。当你定义更复杂的数据结构时,其它类型就出现了。比如,考虑一下value_type是T的链表类 list。链表通常是由节点组成的,每个节点含有一个T类型的对象和一个指向下个节点的指针。于是,作为最初的尝试,我们可能定义链表的节点为:
  1. template <class T>
  2. struct List_node
  3. {
  4.   T val;
  5.   List_node* next;
  6. };
复制代码
把一个新的值加入list的过程看起来可能是这样的:
  • 使用一个value_type是List_node<T>的allocator,为一个新节点分配内存。
  • 使用一个value_type是T的allocator,在节点的val位置上构建新的元素。
  • 将这个节点连接到适当的位置。

这个过程需要处理两个不同的allocator,其中一个是通过对另一个的重绑定而获得的。它对几乎所有程序都工作得很好,即使是将allocator用于相当复杂的目的的程序。它所不能完成的是向allocator提供一些不寻常的指针类型。它明显依赖于能使用List_node<T> *类型的普通指针。

如果你极端野心勃勃,想将其它可能的指针类型传给allocator,一切都突然变得复杂多了--从一个节点指向另一个节点的指针不再是 List_node<T> *或void *了,但它必须是能够从allocator获取的某个类型。直接实现是不太可能的:用一个不完全的类型实例化allocator是非法的,因此,除非 List_node已经被完整定义,否则无法谈论指向List_node的指针。我们需要一个精巧的申明顺序。
  1. template <class T, class Pointer>
  2. struct List_node
  3. {
  4.   T val;
  5.   Pointer next;
  6. };

  7. template <class T, class Alloc>
  8. class List : private Alloc
  9. {
  10. private:
  11.   typedef typename Alloc::template rebind<void>::other Void_alloc;
  12.   typedef typename Void_alloc::pointer Voidptr;
  13.   typedef typename List_node<T, Voidptr> Node;
  14.   typedef typename Alloc::template rebind<Node>::other Node_alloc;
  15.   typedef typename Node_alloc::pointer Nodeptr;
  16.   typedef typename Alloc::template rebind<Voidptr>::other  Voidptr_alloc;

  17.   Nodeptr new_node(const T& x, Nodeptr next) {
  18.     Alloc a = get_allocator();
  19.     Nodeptr p = Node_alloc(a).allocate(1);
  20.     try {
  21.       a.construct(p->val, x);
  22.     }
  23.     catch(...) {
  24.       Node_alloc(a).deallocate(p, 1);
  25.       throw;
  26.     }

  27.     Voidptr_alloc(a).construct(p->next, Voidptr(next));
  28.     return p;
  29.   }
  30.   ...
  31. };
复制代码
最后,以防你认为这么费劲才获得这么小的好处太不值了,提醒一下:仅仅因为你能写一个使用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()。原理上,可以以这样几个不同的方式处理他们:
  • 禁止在两个allocator不等价的容器间进行swap()和splice()。
  • 在swap()和splice()中放入一个allocator等价性的测试。如果不等价,降格调用copy()和赋值运算。
  • 只提供swap():如同对容器中的数据一样,交换它们的allocator。(很难找出如何将它推广到splice上.它同样也带来一个问题:如何swap没有定义赋值运算符的东西?)

如果你能避免对可能具有不同allocator的容器使用swap()和splice(),一切都很安全。在实践中,我没有发现这会造成严重束缚:你需要严格练习安全使用诸如内存池这类特性,而你或许并不想不加选择地混用使用不同allocator的容器。

部分因为不熟悉,部分因为C++标准的要求并不令人满意,目前对allocator的绝大多数的使用都很简单。由于C++社群对allocator变得越来越熟悉,并且标准已被阐明,我们能期待更复杂的使用将会浮现。 Listing 1: A sample allocator based on malloc
  1. template <class T> class malloc_allocator
  2. {
  3. public:
  4.   typedef T                 value_type;
  5.   typedef value_type*       pointer;
  6.   typedef const value_type* const_pointer;
  7.   typedef value_type&       reference;
  8.   typedef const value_type& const_reference;
  9.   typedef std::size_t       size_type;
  10.   typedef std::ptrdiff_t    difference_type;

  11.   template <class U>
  12.   struct rebind { typedef malloc_allocator<U> other; };

  13.   malloc_allocator() {}
  14.   malloc_allocator(const malloc_allocator&) {}

  15.   template <class U>
  16.   malloc_allocator(const malloc_allocator<U>&) {}

  17.   ~malloc_allocator() {}

  18.   pointer address(reference x) const { return &x; }
  19.   const_pointer address(const_reference x) const {
  20. return x;
  21.   }

  22.   pointer allocate(size_type n, const_pointer = 0) {
  23. void* p = std::malloc(n * sizeof(T));

  24. if (!p)
  25.   throw std::bad_alloc();
  26. return static_cast<pointer>(p);
  27.   }

  28.   void deallocate(pointer p, size_type) { std::free(p); }

  29.   size_type max_size() const {
  30. return static_cast<size_type>(-1) / sizeof(T);
  31.   }

  32.   void construct(pointer p, const value_type& x) {
  33. new(p) value_type(x);
  34.   }

  35.   void destroy(pointer p) { p->~value_type(); }

  36. private:

  37.   void operator=(const malloc_allocator&);
  38. };

  39. template<> class malloc_allocator<void>
  40. {
  41.   typedef void        value_type;
  42.   typedef void*       pointer;
  43.   typedef const void* const_pointer;

  44.   template <class U>
  45.   struct rebind { typedef malloc_allocator<U> other; };
  46. };

  47. template <class T>
  48. inline bool operator==(const malloc_allocator<T>&,
  49.                    const malloc_allocator<T>&) {
  50.   return true;
  51. }


  52. template <class T>
  53. inline bool operator!=(const malloc_allocator<T>&,
  54.                    const malloc_allocator<T>&) {
  55.   return false;
  56. }
复制代码
C++ 内存管理(二)----std::alloc C++ 内存管理(二)----std::alloc一、std::alloc运行模式(G2.9)- 基本结构- 运行图示过程二、std::alloc 源码剖析- 第一级分配器- 第二级分配器- refill( )函数- chunk_alloc( )函数三、总结 一、std::alloc运行模式(G2.9) - 基本结构 前面中的per-class allocator的四个版本,实现操作都是每个class里面只有专属于它的allocator,然后其里头也只维护一条free list,专门为其服务。 而std:: 阅读详情

相关推荐

stl allocator源码学习

概述 介绍几个allocator的源码实现:简单的对operator new和operator delete进行封装的实现,vs2015中的实现,STLport中的实现,仿造STLport实现内存池。 1. 参考 http://www.cplusplus.com/reference/memory/allocator/ 《STL源码剖析》 《C++ Primer 第五版》 《Generi

xiyanggudao的博客 7600

C++的空间分配器allocator

一、背景 在STL中,Memory Allocator 处于最底层的位置,为一切容器提供存储服务,是一切其他组件的基石。对于一般使用 STL 的用户而言,Allocator 是不可见的,如果需要对 STL 进行扩展,如编写自定义的容器,就需要调用 Allocator 的内存分配函数进行空间配置。 ......

sinat_31608641的博客 4889

实现自定义的 STL allocator

STL allocator 的注释讲解请见:http://blog.youkuaiyun.com/lifesider/archive/2011/06/06/6527776.aspx 这里实现的自定义allocator是继承自std::allocator,因为两者有公共部分,对于完全重写的allocator,下面的内容就不用读了。 上源码#include templateclas

lifesider 9244

Memory Analyzer (MAT) 使用

The Eclipse Memory Analyzer is a fast and feature-rich Java heap analyzer that helps you find memory leaks and reduce memory consumption. Use the Memory Analyzer to analyze productive heap dumps with hundreds of millions of objects, quickly calculate the

转载技术难题 1427

STL 空间配置器 allocator

`STL` 空间配置器 `allocator``STL` 标准下的 `allocator`神奇的 `SGI STL``SGI` 里效率低下的 `std::allocator``SGI` 里正规的 `std::alloc`构造 `construct()` & 析构 `destroy()``alloc` 设计哲学`alloc` 的第一级配置器:`__malloc_alloc_template``alloc` 的第二级配置器:`__default_alloc_template` 无私奉献的 alloca

weixin_46323890的博客 655

实现自己的STL配置器allocator

实现自己的STL配置器 STL所有数据都存储在容器之中,而容器需要配置空间以置放资料,所以配置器就是完成这一个任务的。 以下是它的一些接口 allocator::value_type //类型,如int allocator::pointer //指针,如int * allocator::const_pointer allocator::reference allocator::const_...

c++ 708

关于STL allocator

关于STL 中allocator的接口与实现,C++标准有比较清楚的定义:allocator wiki 1. SGI STL版本的allocator并没有遵守C++标准。它只提供simple_alloc类共container使用,设计的allocator名字叫做alloc,有二级配置器。第一级配置采用malloc/free来实现allocate/deallocate,第二级配置器采用针对

zeroom的技术专栏 8454

C++ 分配器(Allocator)详解

本文详细介绍了C++分配器(Allocator)的原理与实现。分配器是STL容器内存管理的核心组件,负责内存分配和对象构造解耦。文章首先解析了标准分配器std::allocator的接口和使用方法,包括allocate/deallocate等核心操作。然后重点讲解了自定义分配器的实现规范,通过实例演示了如何开发具有内存追踪功能的分配器,并分析了其在vector扩容过程中的内存行为。最后讨论了内存池优化、C++17新特性及最佳实践,为特定场景下的内存管理优化提供了解决方案。掌握分配器技术能帮助开发者深入理解S

woaibcheng的博客 836

关于STL Allocator

看到一个线程池的实现代码里用到了STL 的Allocator,翻了一篇文章读读。

ThinkHY的专栏 1473

机器学习中正则化项L1和L2的直观理解

正则化(Regularization) 机器学习中几乎都可以看到损失函数后面会添加一个额外项,常用的额外项一般有两种,一般英文称作ℓ1ℓ1\ell_1-norm和ℓ2ℓ2\ell_2-norm,中文称作L1正则化和L2正则化,或者L1范数和L2范数。 L1正则化和L2正则化可以看做是损失函数的惩罚项。所谓『惩罚』是指对损失函数中的某些参数做一些限制。对于线性回归模型,使用L1正则化的模型建叫做...

小平子的专栏 43万+

COMSOL实现裂缝性储层油水两相流模拟的关键技术

多孔介质中的流体流动模拟是油气藏开发的核心技术,其基本原理基于达西定律和质量守恒方程。在裂缝性储层中,离散裂缝网络与基质系统的耦合流动带来了特殊的数值挑战。通过COMSOL Multiphysics平台实现的多物理场耦合技术,可以精确模拟油水两相在复杂裂缝系统中的渗流行为。该技术采用Level Set方法表征裂缝几何形态,结合自适应网格生成技术解决狭窄裂缝的网格剖分难题,并创新性地引入Bang-Larsen毛管压力滞后模型,显著提高了采收率预测精度。这些方法在致密油藏开发、水驱优化等工程实践中具有重要应用价

weixin_34005090的博客 229

浅析STL allocator

转载自 一般而言,我们习惯的 C++ 内存配置操作和释放操作是这样的: 1 class FOO{}; 2 FOO *pf = new FOO; 3 delete pf; 我们看其中第二行和第三行,虽然都是只有一句,当是都完成了两个动作。但你 new 一个对象的时候两个动作是:先调用::operator new 分配一个对象大小的内存,然后在这个内存上调用FOO::FOO()构造对象。同样...

teslali 3000

Hopf振荡器在机器人步态控制里到底怎么用?一个CPG仿生案例说清楚

本文深入解析了Hopf振荡器在机器人步态控制中的实际应用,通过CPG仿生案例详细展示了从数学方程到关节指令的完整流程。重点探讨了Hopf振荡器的极限环稳定性、参数调节技巧及自适应控制策略,为机器人运动控制提供了高效解决方案。

weixin_29284657的博客 202

深入理解 STL allocator

由于std::set,std::multiset,std::map,std::multimap四种容器的插入删除操作性能高并且自动排序,在很多时候比如需要动态操作时往往会使用它们,然后由于容器内部使用的是节点,每次的插入或删除都要调用new或delete,往往容易造成碎片和性能下降,于是自定义的allocator出现了。这篇文章的目的就是详细讲解std::allocator的内部结构,为实现自

lifesider 1万+

使用fairseq从头开始训练一个中英神经机器翻译模型

前言 由于毕设是做神经机器翻译相关,所以先尝试一下神经机器翻译的整个流程是非常有必要的。故将在news-commentary-v15语料上训练有监督中英NMT模型的整个流程,包括工具和数据的准备、数据的预处理、训练及解码等过程,以及过程中遇到的问题和解决方案记录在此,以供后期回顾,也希望能够给予别人一些帮助。附本人博客 1 相关工具及目录结构 1.1 相关工具 除jieba是使用pip install安装外,其他几个工具都是建议直接克隆库到自己的用户目录中,方便使用其脚本(moses/subword-nmt

qq_42734797的博客 2万+

802.1AS时间同步机制深度解析:从Sync报文到多域冗余设计

本文深度解析了802.1AS时间同步机制,从Sync与Follow_Up报文的核心交互原理入手,详细阐述了其如何通过精确的时钟频率比测量、链路延迟计算和驻留时间补偿来实现微秒乃至纳秒级同步。文章进一步探讨了802.1AS-2020版本引入的多时钟域冗余设计,为构建高可靠的车载TSN网络提供了关键见解和实战配置指南。

pytorch8learner的博客 460

STL中的空间配置器allocator的实现原理及源码剖析

allocator是STL为自己的模版容器所设计的标准空间配置器。处于std命名空间下 今天我要讲的的allocator的源码在我的github上,地址为 mySTL 是我模仿标准STL写出的代码,并添加了注释。allocator简介allocator为一个模版类,我们使用的时候一般都会将其绑定到一个具体的类型上。如下:allocator<int> alc;allocator大致需要实现如下功能:

我的大学 4092

数学建模学习(78):多输入多输出回归预测模型(结合XGBoost实现)

多输入多输出回归预测问题求解

全栈川川 8130

STL 配置器(allocator)

配置器:负责空间配置与管理,从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的 class template。 空间配置器:整个 STL 的操作对象(所有的数值)都存放在容器之内,而容器一定需要配置空间以存放内容。 什么是 allocator?allocator 有什么用? 我们需要对 C++ 的 allocator 的堆内存接口调用顺序有个清晰的认识,如下图所示。 allocator 堆内存管理接口 STL 的容器(eg: vector、stack、deque等)有一个共同特征,就是

南宫封清的博客 1353

YOLOv10改进策略【模型轻量化】| 替换骨干网络为MoblieNetV2,含模型详解和完整配置步骤

本文记录的是。采用深度可分离卷积将标准卷积分解为深度卷积和1×1卷积。同时,引入来,避免非线性层导致的性能下降问题。本文将应用到YOLOv10中,借助其高效的结构和特性,在保持一定精度的前提下,显著降低YOLOv10的计算复杂度和内存占用。

Limiiiing的博客 533

【YOLOv8/v9/v10 实战 01】YOLOv8/v9/v10全系列实战对决:性能矩阵、架构拆解与2026部署指南

2026年YOLO生态已进入“战国时代”,Ultralytics YOLOv8/11/26、阿里YOLO12、清华YOLOv10、微软YOLOv9四大路线并行发展。本文基于官方文档、学术论文及实测数据,系统剖析三大核心版本(YOLOv8/v9/v10)的架构演进逻辑,构建覆盖参数量、mAP、FPS、FLOPs的完整性能矩阵。通过8组工业场景实测、全流程代码实现(训练-导出-部署)、6类硬件适配方案,解决新手选型难、进阶者优化无门、工业开发者部署低效的痛点。

专注于人工智能、软件开发、工控自动化、工厂数字化及智能化等领域,希望和大家共同进步! 534

C++ STL——allocator

C++ 标准模板库提供了一个非常重要的内存管理机制——allocator。它对内存管理接口进行了封装,实现了对象内存分配与构造分离、对象析构与内存释放分离。标准库中通过模板参数为容器提供内存管理策略,allocator 就是一种具体的策略,我们可以实现基于共享内存、内存池的 自定义 allocator 把它们应用于容器。本文章将以 MSVC 的 allocator 框架为参考,讲述 allocator 的核心工作原理。

ypy9323的博客 597

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

stl allocator

stl allocator实现

sjc960115的博客 547

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

续STL之allocator

针对CPP的标准模版库的空间配置器而写的文章,通过实现一个自己的配置器了解容器底层空间操作原理

thirtythird的博客 525

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

STL中的Allocator

最近在看STL,写一点博客。 关于一些细节可能就不放上来了,就写一些我自己的心得。 1.为什么要用Allocator C++容器的设计思路是,对存放到里面的东西是拷贝一份放进去。 而且里面的空间都是动态分配的。 所以在构建容器的时候,需要必要的①内存分配②构造对象。 2.new operator、operator new、placement new

山鸡哥的专栏 1367
上一篇: 谈谈Windows程序中的字符编码
下一篇: Perl 发送http 请求。
hardworkba
博客等级 码龄18年 11粉丝 20原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值