Effective STL [11] | 理解自定义分配器的正确用法

警告
本文最后更新于 2023-09-13,文中内容可能已过时。

很多时候,你会有建立自定义分配器的想法:

  • allocator<T> 对线程安全采取了措拖,但是你只对单线程的程序感兴趣,你不想花费不需要的同步开销
  • 在某些容器里的对象通常一同被使用,所以你想在一个特别的堆里把它们放得很近使引用的区域性最大化
  • 你想建立一个相当共享内存的唯一的堆,然后把一个或多个容器放在那块内存里,因为这样它们可以被其他进程共享。

假定你有仿效 mallocfree 的特别程序,用于管理共享内存的堆

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
template<typename T>
class SharedMemoryAllocator {
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);
    }
...
};

使用 SharedMemoryAllocator

1
2
3
4
5
6
7
8
// 方便的typedef
typedef vector<double, SharedMemoryAllocator<double> >
SharedDoubleVec;
...
{ // 开始一个块
SharedDoubleVec v; // 建立一个元素在共享内存中的vector
... // 结束这个块
}

「问题:」v 使用 SharedMemoryAllocator,所以 v 分配来容纳它元素的内存将来自共享内存,但 v 本身 —— 包括它的全部数据成员 —— 几乎将肯定不被放在共享内存里,v 只是一个普通的基于堆的对象,所以它将被放在运行时系统为所有普通的基于堆的对象使用的任何内存。那几乎不会是共享内存。

为了把 v 的内容和 v 本身放进共享内存,必须这么做:

1
2
3
4
5
6
void *pVectorMemory = mallocShared(sizeof(SharedDoubleVec));// 分配足够的共享内存来容纳一个SharedDoubleVec对象
SharedDoubleVec *pv = new (pVectorMemory) SharedDoubleVec; // 使用“placement new”来 在那块内存中建立 一个SharedDoubleVec对象;
// 参见下面这个对象的使用(通过pv)
...
pv->~SharedDoubleVec(); // 销毁共享内存中的对象
freeShared(pVectorMemory); // 销毁原来的共享内存块

这就是「手工的四步分配/建造/销毁/回收的过程」:获得一些共享内存 ——> 在里面建立一个用共享内存为自己内部分配的 vector ——> 用完这个 vector 时,调用它的析构函数 ——> 释放 vector 占用的内存。

这段代码有 2 点需要注意:

  • 忽略了 mallocShared 可能返回一个 null 指针。

  • 共享内存中的 vector 的建立由 “placement new” 完成。

假设有 2 个堆,类 Heap1Heap2

每个堆类有用于进行「分配」和「回收」的「静态成员函数」:

1
2
3
4
5
6
7
8
9
class Heap1 {
public:
...
    static void* alloc(size_t numBytes, const void *memoryBlockToBeNear);
    static void dealloc(void *ptr);
...
};

class Heap2 { ... }; // 有相同的alloc/dealloc接口

你想在不同的堆里联合定位一些 STL 容器的内容。

首先,设计一个分配器,使用像 Heap1 和 Heap2 那样用于真实内存管理的类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 来把容器的元素集合在一起:

1
2
3
4
vector<int, SpecificHeapAllocator<int, Heap1 >> v; // 把v和s的元素放进Heap1
set<int, SpecificHeapAllocator<int Heap1 >> s;
list<Widget, SpecificHeapAllocator<Widget, Heap2>> L; // 把L和m的元素 放进Heap2
map<int, string, less<int>, SpecificHeapAllocator<pair<const int, string>, Heap2>> m;

在这个例子里,很重要的一点是「Heap1 和 Heap2 是类型而不是对象」。

STL 为用不同的分配器对象初始化相同类型的不同 STL 容器提供了语法。那是「因为如果 Heap1 和 Heap2 是对象而不是类型,那么它们将是不等价的分配器,那就违反了分配器的等价约束」。

只要遵循「相同类型的所有分配器都一定等价的限制条件」,你将毫不费力地使用自定义分配器来「控制一般内存管理策略,群集关系和使用共享内存以及其他特殊的堆」。

Buy me a coffee~
Jian YE 支付宝支付宝
Jian YE 微信微信
0%