现代C++中的类型抹除分配器

10

"经典"的STL容器如std::vectorstd::map将它们的分配器类型作为模板参数。这意味着std::vector<T, std::allocator<T>>std::vector<T, MyAllocator>被认为是完全不同的类型。

另一方面,一些较新的支持分配器的类,如std::shared_ptrstd::tuple,使用类型擦除来“隐藏”关于分配器的信息,因此它不会成为类型签名的一部分。然而,std::unordered_map(与shared_ptr类似)保持了采用额外默认模板参数的经典方法。

问题:

  1. std::vector<T, std::allocator<T>>std::vector<T, MyAllocator>视为不同类型是否可取,还是仅仅是类型擦除在STL编写时不是一个众所周知的技术的副作用?

  2. 以这种方式使用类型擦除是否存在缺点(如果有)?

  3. 在新的容器中应始终首选类型擦除分配器吗?

答案:
  1. std::vector<T, std::allocator<T>>std::vector<T, MyAllocator>视为不同类型是有意义的,因为这样可以确保在编译时对分配器进行静态类型检查,从而避免了错误的使用。

  2. 使用类型擦除可能会导致一些运行时开销,因为需要动态分配内存来存储分配器对象。此外,它也可能会限制对分配器的直接访问。

  3. 是否应该使用类型擦除分配器取决于具体情况。如果需要快速原型设计或不需要特定的分配器功能,则可以考虑使用类型擦除。否则,显式指定分配器是更好的选择。


2
附注:泛型类型擦除分配器实际上是在库基础 TS中提出的。 - user657267
@user657267:我不知道这个,谢谢你提供的信息,我会去看一下 :-) - Tristan Brindle
5
使用类型擦除的分配器的缺点通常是性能。当编译器可以看到所有内容时,它可以内联例程等。当您拥有类型擦除的东西时,它只能调用例程。 - Marshall Clow
1
进一步回应马歇尔的评论……当你有理由关心时,通常可以编写一个分配器类型,内部使用类型擦除来调度到各种具体的分配器之一,从而允许您使用相同的外部分配器并避免不同的容器实例化。两全其美。 - Tony Delroy
1个回答

8
一些新的分配器感知类如std::shared_ptrstd::tuple使用类型擦除来“隐藏”有关分配器的信息,因此它不构成类型签名的一部分。 std::tuple根本不使用类型擦除。元组可以用分配器构造,但它只是(有条件地)将其传递给其元素,它不会在任何地方存储它,因为元组从不分配任何内存,因此不需要分配器。 std::shared_ptr确实分配内存,因此它可以使用分配器,直到控制块需要被释放为止。由于控制块已经对用户不可见并存储在堆上,与该控制块相关联的分配器也对用户不可见。
因此,与shared_ptr的比较并不是非常相关,因为它具有完全不适用于容器的分配器的完全不同用途。
STL中使用分配器的最初动机是封装有关内存模型的细节,特别是分段内存的“近”和“远”指针。这就是为什么分配器定义了一个pointer成员,容器在内部使用它。使用近指针的向量不能将其元素的地址与另一个使用远指针的容器中的地址混淆,例如。
因此,对于最初的用途,具有不同的类型是有价值的,但这种最初的用途现在已经不相关了。
在这种方式中使用类型擦除的缺点(如果有)是什么?
  • 所有函数调用都必须是虚拟的(或者其他形式的间接调用,例如通过函数指针),并且更难进行内联。这对于shared_ptr来说不是问题,它只需在擦除分配器类型之前分配一些内存,然后再使用一次以释放内存,但是通用容器可能会进行数千次分配。

  • 类型擦除的分配器很难从容器中检索,使得创建容器的副本变得复杂。(它应该使用分配器的副本吗?如何复制您看不见的东西?)这对于像shared_ptr这样的类型不是问题,因为复制shared_ptr只是增加引用计数,而不会分配任何内容。

  • 对象通常需要比sizeof(void*)更大的空间来存储类型擦除的分配器。即使分配器是一个空的无状态类型,如std::allocator<T>,那个额外的指针也无法优化掉。根据类型的不同,这可能意味着与利用空基类优化存储空的分配器相比,大小增加了50%甚至100%。这对于shared_ptr不是问题,因为除了在创建或销毁控制块时需要分配器之外,shared_ptr不需要使用它进行其他(de)分配。

  • 因为类型擦除的分配器必须满足特定的抽象接口,所以它必须在其allocatedeallocate成员中使用原始指针。这意味着您不能使用自定义的pointer类型,例如存储相对于基地址的偏移量的指针,这对于在Boost.Interprocess中使用的共享内存分配器非常有用。

  1. 新容器是否总是应该优先考虑使用类型擦除的分配器?

我认为不是。如果分配器是类型的一部分,则可以针对常见情况优化它,同时仍允许容器的用户选择使用内部使用类型擦除的多态分配器,例如Library Fundamentals TS中的分配器。


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