STL容器:构造函数的分配器参数和作用域分配器

14

STL容器有一个模板参数,可以选择自定义分配器。花了一些时间,但我认为我理解了其工作原理。不过这样做并不是很好,因为给定的分配器类型并没有直接使用,而是重新绑定到另一种类型的分配器上。最终我可以使用它。

在阅读API后,我发现还可以将分配器作为构造函数参数传递。但是,如果容器内部重新绑定了来自模板参数的给定分配器,我怎么知道它使用的是哪种类型的分配器呢?

此外,我阅读到C++11现在使用作用域分配器,允许重用容器的分配器用于其包含的容器。具有作用域分配器的容器的实现大致与不知道作用域容器的容器有何不同?

不幸的是,我找不到任何能够解释这个问题的资料。谢谢回答!

2个回答

13
但是,如果容器从模板参数中内部重新绑定给定的分配器,我如何知道容器使用哪种类型的分配器呢?
在构造函数中始终提供一个Allocator(其中T是容器的value_type)。容器会将其转换为必要的Allocator,其中U是容器的某些内部数据结构。Allocator需要提供这样的转换构造函数,例如:
template <class T> class allocator {
    ...
    template <class U> allocator(const allocator<U>&);

此外,我了解到C++11现在使用作用域分配器(scoped allocators),其允许重复使用容器的分配器用于其包含的容器。
更准确地说,C++11有一个名为`scoped_allocator_adaptor`的分配器适配器。
template <class OuterAlloc, class... InnerAllocs>
class scoped_allocator_adaptor : public OuterAlloc
{
    ...
};

从C++11开始:

scoped_allocator_adaptor类模板是一个分配器模板,它指定了容器要使用的内存资源(外部分配器),并且还指定了要传递给容器中每个元素构造函数的内部分配器资源。该适配器使用一个外部和零个或多个内部分配器类型进行实例化。如果只使用一个分配器类型进行实例化,则内部分配器变为scoped_allocator_adaptor本身,从而对于容器及其内部的每个元素以及如果元素本身是容器,则递归地对它们的每个元素使用相同的分配器资源。如果使用多个分配器进行实例化,则第一个分配器是容器使用的外部分配器,第二个分配器被传递到容器元素的构造函数中,如果元素本身是容器,则第三个分配器被传递到元素的元素中,以此类推。如果嵌套容器的深度大于分配器数量,则最后一个分配器将重复使用,如单分配器情况下一样。[注意scoped_allocator_adaptor派生自外部分配器类型,因此可以在大多数表达式中替换外部分配器类型。 - 注解]

因此,只有当您将scoped_allocator_adaptor指定为容器的分配器时,才会获得作用域分配器的行为。

启用作用域分配器的容器实现如何与不知道作用域容器的容器大致不同?

关键在于容器现在通过一个名为allocator_traits的新类处理其分配器,而不是直接处理分配器。并且容器必须对某些操作使用allocator_traits,例如在容器中构造和销毁value_type。容器不得直接与分配器交互。

例如,分配器可以提供一个名为construct的成员,该成员将使用给定的参数在特定地址上构造类型:

template <class T> class Allocator {
     ...
    template<class U, class... Args>
        void construct(U* p, Args&&... args);
};

如果分配器没有提供这个成员,allocator_traits将提供一个默认实现。无论如何,容器必须使用这个construct函数构造所有的value_type,但是通过allocator_traits使用它,而不是直接使用allocator
allocator_traits<allocator_type>::construct(the_allocator, *ugly details*);
scoped_allocator_adaptor提供自定义的construct函数,这些函数将被allocator_traits转发利用uses_allocator特性,并传递正确的分配器给value_type构造函数。容器对这些细节一无所知,它只需要知道必须使用allocator_traits construct函数构造value_type
容器必须处理更多细节以正确处理有状态的分配器。通过让容器不做任何假设,而是通过allocator_traits获取所有属性和行为来处理这些细节。甚至容器也无法假设pointerT*。相反,该类型是通过询问allocator_traits找到的。
简而言之,要构建C++11容器,请研究allocator_traits。然后,当您的客户使用scoped_allocator_adaptor时,您可以免费获得作用域分配器行为。

4
容器使用的分配器类型由其构造函数参数定义:容器的构造函数所期望的就是这种类型。然而,任何分配器都需要能够为不同于其所定义类型的类型提供服务。例如,对于 std::list<T, A>,期望的分配器可以分配 T 对象,但它将从未用于分配这些对象,因为 std::list<T, A> 实际上需要分配节点。也就是说,分配器将被重新绑定以分配不同的类型。遗憾的是,这使得使用分配器来服务特定类型变得困难:您不知道分配器实际上会服务的类型。
关于作用域分配器,它的工作方式非常直接:容器确定是否有任何成员具有接受匹配分配器的构造函数。如果是这种情况,它将重新绑定已使用的分配器,并将该分配器传递给成员。不那么直接的逻辑是确定是否使用了分配器。为了确定成员是否使用分配器,使用 traits std::uses_allocator<T, A> :它确定是否 T 具有嵌套的 typedef allocator_type ,并且是否可以将 A 转换为此类型。有关如何构造成员对象的规则,请参见 20.6.7.2 [allocator.uses.construction]。
实际上,这意味着分配器对于处理容器及其成员使用的池非常有用。在某些情况下,当分配相似大小的对象时,例如对于任何基于节点的容器,保留相同大小的对象池可能也很合理。但是,从使用分配器的模式中并不清楚它们是否用于节点或包含有一些字符串之类的内容。此外,由于使用不同的分配策略会更改类型,因此最合理的方法要么是坚持使用默认分配,要么是使用代表实际定义分配策略的多态分配器的分配器类型。当然,一旦您拥有有状态的分配器,您可能会拥有具有不同分配器的对象,并且例如,交换它们可能无法正常工作。

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