看到过几次,但一直对allocator
的功能不甚了解。最近在看《Effective STL》的时候,看到里面提到了allocator
以及示例代码,决定尝试去理解一下。
allocator
本质是:在每个新的对象实例被加入到容器中时,容器所作的准备工作。如果你以为新添加对象实例时,容器只需要在insert
函数中确认一下空间并new
一个对象,那就大错特错了。
以list
为例,容器在新添加对象的时候,除了为对象保留足够的空间外,还需要创建指向上个结点与下个结点的指针。这种在每次添加新对象实例时都具备的重复工作,就由allocator
完成。
我用User-Defined Allocator中的代码进行了测试。PS:这段代码太棒了,不足百行,简单清晰地演示了一般allocator
需要完成的工作。
//自定义的allocator
namespace MyLib {
template <class T>
class MyAlloc {
public:
// type definitions
typedef T value_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
// rebind allocator to type U
template <class U>
struct rebind {
typedef MyAlloc<U> other;
};
// return address of values
pointer address(reference value) const {
return &value;
}
const_pointer address(const_reference value) const {
return &value;
}
/* constructors and destructor
* - nothing to do because the allocator has no state
*/
MyAlloc() throw() {
}
MyAlloc(const MyAlloc&) throw() {
}
template <class U>
MyAlloc(const MyAlloc<U>&) throw() {
}
~MyAlloc() throw() {
}
// return maximum number of elements that can be allocated
size_type max_size() const throw() {
return std::numeric_limits<std::size_t>::max() / sizeof(T);
}
// allocate but don't initialize num elements of type T
pointer allocate(size_type num, const void* = 0) {
// print message and allocate memory with global new
std::cerr << "allocate " << num << " element(s)"
<< " of size " << sizeof(T) << std::endl;
pointer ret = (pointer)(::operator new(num*sizeof(T)));
std::cerr << " allocated at: " << (void*)ret << std::endl;
return ret;
}
// initialize elements of allocated storage p with value value
void construct(pointer p, const T& value) {
// initialize memory with placement new
std::cout << "allocator construction" << std::endl;
new((void*)p)T(value);
}
// destroy elements of initialized storage p
void destroy(pointer p) {
// destroy objects by calling their destructor
std::cout << "allocator destruction" << std::endl;
p->~T();
}
// deallocate storage p of deleted elements
void deallocate(pointer p, size_type num) {
// print message and deallocate memory with global delete
std::cerr << "deallocate " << num << " element(s)"
<< " of size " << sizeof(T)
<< " at: " << (void*)p << std::endl;
::operator delete((void*)p);
}
};
// return that all specializations of this allocator are interchangeable
template <class T1, class T2>
bool operator== (const MyAlloc<T1>&,
const MyAlloc<T2>&) throw() {
return true;
}
template <class T1, class T2>
bool operator!= (const MyAlloc<T1>&,
const MyAlloc<T2>&) throw() {
return false;
}
}
我在原代码的destroy()
和construct()
函数中各加了一行输出,以便我们能在测试中观察到他们何时被调用。
同时用以下代码进行测试
class object {
int i;
public:
object(int i) :i(i) {
std::cout << "object construction" << std::endl;
};
object(const object& t):i(t.i) {
std::cout << "copy construction" << std::endl;
}
~object() {
std::cout << "object destruction" << std::endl;
}
};
int main()
{
// create a vector, using MyAlloc<> as allocator
std::vector<object, MyLib::MyAlloc<int> > v;
std::cout << "begin" << std::endl;
//使用push_back()的测试结果大同小异,大家可以自己尝试
v.emplace_back(42);
std::cout << "exit" << std::endl;
}
结果如下:
allocate 1 element(s) of size 8
allocated at: 007421C0
allocator construction
begin………………………………………………………………….
allocate 1 element(s) of size 4
allocated at: 00741C60
object construction
allocator construction
copy construction
object destruction
exit………………………………………………………………………
allocator destruction
object destruction
deallocate 1 element(s) of size 4 at: 00741C60
allocator destruction
deallocate 1 element(s) of size 8 at: 007421C0
begin和exit后的点是我为了使结果呈现地更清晰而添加的,并非源代码输出,但这无关紧要。
结果分析
从源代码中可以得知,object对象的size只有4个字节(一个int
),但在begin之前的1-3行,程序已经调用allocator
分配了一个8字节的空间。说明这不是用来存放对象实例的。推测是vector
用来存放容器成员变量(很可能是size)的地方。在第3行调用MyAlloc::construct()
后,没有调用object
的构造函数也说明了该处并没有存放任何对象实例。
从begin后的第5行开始就是向容器添加对象实例的过程了。
第5,6两行都是调用MyAlloc::allocate()
的结果,创造了一个4字节的空间,并返回一个指针指向该空间。
第7行,程序调用对应构造函数创造了object
对象。
第8行,程序调用了MyAlloc::construct()
。
第9行,程序调用了object
的复制构造函数,并将对象放在之前申请到的内存上。这与我们以往的认知:emplace_back()
函数直接在目标内存上创建对象 不一致。它依旧是先构造了一个对象,在把它复制到容器内。为什么跟以往说明不一致,我也不知道。
第10行是一个析构函数,可想而知这显然是第7行构造的对象,对容器来说,已经得到副本了,就把“原配”丢弃了。
至此,容器完成了一次添加对象的过程。
如果逐步添加对象时,会遇到需要扩展vector长度的时候,这时可能会发现,虽然可能只是添加1个对象,但输出的却是 添加了XXX个对象,随后又 删除了XXX-1个对象。如果预先reserve()
足够的空间,就不会出现这个情况。大家可以自行测试。
在第11行的exit后,就是容器的析构过程。
第12行先调用MyAlloc::destroy()
。
第13行调用object::~object()
析构了容器内的对象。
第14行调用MyAlloc::deallocate()
释放了之前申请的容纳该元素的空间。对于vector来说,该空间的大小与对象大小一致。但如前文提到的,对于list或set等结构来说,这两者的空间就不一样了。
第15,16行,最后的最后,容器释放了最开始(begin之前)申请的那部分空间。至此容器完全析构。
尾声:这个测试只能对allocator的大致功能有个初步的了解,具体的,rebind::other
是如何发挥作用的,我还没有理解。等明白了再来补充。
顺便,不知道为什么,上述测试代码,如果采用list
替换vector
容器,那么`emplace_back()
函数就能正常发挥作用(而不调用复制构造函数)。