“allocate_shared”是如何工作的?

4

来自cppreference中的std::allocate_shared

使用args作为T构造函数的参数列表,构造一个T类型的对象,并将其包装在std::shared_ptr中。该对象的构造方式类似于表达式::new (pv) T(std::forward<Args>(args)...),其中pv是一个内部的void*指针,用于存储适合容纳T类型对象的内存。该存储通常比sizeof(T)大,以便为共享指针的控制块和T对象使用一个分配。

所有内存分配都使用alloc的副本完成,alloc必须满足Allocator的要求。

我感到困惑的是,考虑下面的用户定义的分配器,同样来自cppreference

template <class T>
struct Mallocator {
    typedef T value_type;
    Mallocator() = default;
    template <class U> Mallocator(const Mallocator<U>&) {}
    T* allocate(std::size_t n) { return static_cast<T*>(std::malloc(n * sizeof(T))); }
    void deallocate(T* p, std::size_t) { std::free(p); }
};
template <class T, class U>
bool operator==(const Mallocator<T>&, const Mallocator<U>&) { return true; }
template <class T, class U>
bool operator!=(const Mallocator<T>&, const Mallocator<U>&) { return false; }

由于Mallocator只能分配sizeof(T)大小的内存,那么allocate_shared如何分配比sizeof(T)更大的存储空间,以便将一个分配用于共享指针的控制块和T对象?


它可以分配2*sizeof(T)的空间,但我不知道它是否真的这样做了。 - user253751
@immibis 当 T=char 时,这可能不太有效 ;) - cdhowie
1个回答

5
这是一个两部分的回答。首先,我将解释它能做什么,然后解释如何做到。
目标是在同一次分配中分配控制块和的空间。这可以通过使用内部模板结构来实现,例如:
template <typename T>
struct shared_ptr_allocation {
    shared_ptr_control_block cb;
    typename std::aligned_storage<sizeof(T)>::type storage;
};

(假设存在一个内部的shared_ptr_control_block类型。我不认为标准要求使用任何特定的结构,这只是一个示例,可能适用于实际实现。)
因此,所有std :: allocate_shared()需要做的就是分配一个shared_ptr_allocation ,它同时获取控制块和T存储器,后者将在稍后使用placement-new初始化。
但是,我们如何获得适用于分配此结构的分配器?我相信这是您问题的关键,答案非常简单:std :: allocator_traits。
此特性具有rebind_alloc模板成员,可用于获取由您自己的分配器构建的不同类型的分配器。例如,allocate_shared的前几行可能如下所示:
template<class T, class Alloc, class... Args>
shared_ptr<T> allocate_shared(const Alloc& alloc, Args&&... args)
{
    using control_block_allocator_t =
        typename std::allocator_traits<Alloc>
                    ::rebind_other<shared_ptr_control_block<T>>;

    control_block_allocator_t control_block_allocator(alloc);

    // And so on...
}

control_block_allocator用于执行实际的分配。
请查看此示例,在进行分配时显示T类型的名称,然后使用std::allocate_shared分配int。当int的编码类型名称为i时,我们正在分配的编码类型名称是St23_Sp_counted_ptr_inplaceIi10MallocatorIiELN9__gnu_cxx12_Lock_policyE2EE。很明显,我们正在分配不同的东西!
另外:我们可以通过前向声明一个分配器模板并将其专门化为一种类型来加以确认,基本上留下其他专门化而没有定义,因此不完整。当我们尝试使用该分配器进行allocate_shared时,编译器应该会发生错误,果然如此
引用块: error: implicit instantiation of undefined template 'OnlyIntAllocator<std::_Sp_counted_ptr_inplace<int, OnlyIntAllocator<int>, __gnu_cxx::_Lock_policy::_S_atomic> >' 因此,在这个实现中,std::_Sp_counted_ptr_inplace是同时持有控制块和对象存储的模板结构体。
现在,为了解决重新绑定实际上如何运作的问题,这里有两个关键要求,而这个非常简单的分配器满足这两个要求。首先,我们需要std::allocator_traits<...>::rebind_other<...>确实有效。来自cppreference文档
rebind_alloc<T>:如果存在,则为Alloc::rebind<T>::other,否则如果此AllocAlloc<U, Args>,则为Alloc<T, Args>
由于此示例类型没有rebind模板成员,因此此重新绑定模板仅从Mallocator <whatever>中剥离模板参数,并将whatever替换为新类型(如果有后续模板参数,则保留它们--在这种情况下没有)。
但是为什么要用旧的来构建新的分配器,这样会有什么作用呢?你自己链接的同一页上已经解释了这个问题:

A a(b): 构造函数使得 B(a)==b 并且 A(b)==a。不会抛出异常。(注意:这意味着所有通过重新绑定相关的分配器都会维护彼此的资源,如内存池)


非常感谢!我明白了。所以,如果我想要实现一个大小为T的池,我需要预先分配一组内存,其大小为sizeof(internal_type<T>),由于我不知道内部类型是什么,也许我可以使用sizeof(T)+32? - alpha
@alpha 如果你想做坏事,你可以像这样(http://coliru.stacked-crooked.com/a/f7fefd18c896f50b)检测大小。 - cdhowie
我明白了。我可以在程序的初始化函数中调用一个虚拟的allocate_shared函数来获取大小,然后分配一组内存。 - alpha

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