条款11:理解自定义分配器的正确用法

优质
小牛编辑
134浏览
2023-12-01

条款11:理解自定义分配器的正确用法

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

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

void* mallocShared(size_t bytesNeeded);
void freeShared(void *ptr); 

并且你希望能把STL容器的内容放在共享内存中。没问题:

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

	void deallocate(pointer ptrToMemory, size_ type numObjects)
	{
		freeShared(ptrToMiemory);
	} 
	... 
};

allocate里的pointer类型、映射和乘法的更多信息参见条款10。

你可以像这样使用SharedMemoryAllocator:

// 方便的typedef
typedef vector<double, SharedMemoryAllocator<double> >
			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<typenameT, typename Heap>
class SpecificHeapAllocator {
public:
	pointer allocate(size_type numObjects, const void *localityHint = 0)
	{
		return static_cast<pointer>(Heap::alloc(numObjects * sizeof(T), 
							localityHint));
	}

	void deallocate(pointer ptrToMemory, size_type numObjects)
	{
		Heap::dealloc(ptrToMemory);
	}
	...
};

然后你使用SpecificHeapAllocator来把容器的元素集合在一起:

vector<int, SpecificHeapAllocator<int, Heap1 > > v;			// 把v和s的元素
set<int, SpecificHeapAllocator<int Heap1 > > s;			// 放进Heap1

list<Widget,
		SpecificHeapAllocator<Widget, Heap2> > L;		// 把L和m的元素
map<int, string, less<int>,						// 放进Heap2
		SpecificHeapAllocator<pair<const int, string>,
					Heap2> > m;

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

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