在具有预分配块的自定义STL分配器中重新绑定

18
我将构建一个自定义分配器,预先分配一个大块(数组)用于存储某个类T的N个元素,然后只需增加数组内的索引以处理分配请求。
由于我不希望对预分配块中的元素进行任何初始化,因此以下内容不可行:
T buffer[N];

因为在这种情况下,T的构造函数将被调用N个块元素。

由于我理解std::aligned_storage不会调用T的构造函数,所以我考虑使用std::aligned_storage,类似于以下方式:

std::aligned_storage<
    N * sizeof(T),
    std::alignment_of<T>::value 
>::type buffer;

T* base = static_cast<T*>( static_cast<void*>(&buffer) );

然后,当请求分配T时(直到(base + N)),分配器只需增加基指针,并在需要时使用就地放置 new 构造T。
我想使用此方案为STL容器定义自定义分配器。但是,对于重新绑定,我认为这里可能会有问题。事实上,如果我理解正确,STL分配器应支持从类型T重新绑定到类型U。例如,因为像std::list<T>(或其他基于节点的容器,如std::map)这样的容器使用分配器来分配实际上不是类型T而是不同类型U的节点(包含T和其他“头”开销信息的节点)。
那么,前面提到的std::aligned_storage方法是否适用于重新绑定?或者(正如我所想的那样),T的正确对齐并不意味着另一种不同类型U的正确对齐?
如何解决这个问题?
如何定义前面提到的buffer,使其也适用于重新绑定到某些不同类型的U
这个问题应该从不同的角度解决吗?如果是,那是什么?

你能否只使用std::alignment_of<std::max_align_t>::value,以使其适合 C++ 标准分配器支持的任何类型,并正确对齐吗?当然,这对于具有特殊(更严格)对齐要求的类型不起作用(最好的例子是 SSE),但那些类型最终始终是问题,即使对于标准分配器也是如此。 - Christian Rau
1个回答

13

你正在朝着正确的方向前进。

一个令人烦恼的细节是,分配器的副本必须相等,即使转换(重新绑定)副本也一样。 相等意味着它们可以释放彼此的指针。 因此,像std::list<int>这样的容器将重新绑定your_alloc<int>your_alloc<node<int>>,然后使用your_alloc<int>构造your_alloc<node<int>>。并且在技术上,您的your_alloc<node<int>>必须释放由your_alloc<int>分配的指针。

这里是我的尝试,以满足此要求。随意复制/修改/滥用此代码。 我的意图是教育,而不是成为世界分配器供应商(这实际上也不会很有利可图:-))。

这个例子采取了一个稍微不同的对齐方式:我碰巧知道,在我关注的平台(OS X、iOS)上malloc返回16字节对齐的内存,所以我的自定义分配器只需要返回这个。 您可以将该数字更改为适合您系统的任何值。

这种硬编码对齐意味着单个池可以安全地提供多个allocator<int>allocator<node<int>>,因为它们都是16字节对齐的(而且这足够了)。 它还意味着副本,甚至转换后的副本,可以检测指针是否指向缓冲区,因为所有副本共享同一个缓冲区。

稍微不同地说,C++委员会实际上已经指定分配器是引用类型:副本是等效的,并指向相同的内存池。

你可以通过欺骗来占用实际嵌入到分配器中的内存池,但只有在某些实现上的某些容器上才能这样做,并且您无法引用标准来支持它。


非常感谢您的建议和分享您的示例分配器代码。 - Mr.C64
1
所以,如果你想要从 stack_allocator 获得 64 字节对齐(例如为了缓存对齐),那么你是否也需要重新定义全局的 ::operator new 来提供相同的 64 字节对齐呢? - TemplateRex
1
@rhalbersma,你可以这样做,或者你可以从::operator new请求额外的64-alignof(std::max_align_t),然后只在64字节边界上开始使用它(但你需要以某种方式将原始未对齐指针传回::operator delete)。 - Jonathan Wakely
我知道在我的感兴趣的平台(OS X,iOS)上,malloc返回16字节对齐的内存,因此我的自定义分配器只需要返回这个。实际上,通过使用alingof(std::max_align_t),这应该很容易做到跨平台(即使这在理论上可能性较小,而且16可能总是一个很好的值)。 - Christian Rau
@JonathanWakely 那么唯一的选择就是进行簿记并存储指针(即,对齐指针a实际上是::new指针b,可以被::delete删除)?显然,如果我们不必重新绑定分配器以支持各种大小的不同对象的存储,则这不会成为问题。 - Red XIII
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接