allocate_shared 如何工作?

alp*_*pha 4 c++ c++11

来自cppreference 中的 std::allocate_shared

构造一个 类型的对象T并将其包装在std::shared_ptrusing中args作为 的构造函数的参数列表T。该对象就像通过表达式构造的::new (pv) T(std::forward<Args>(args)...),其中是指向适合保存该类型的对象的存储的pv内部指针。存储空间通常大于共享指针的控制块和对象的一次分配。void*Tsizeof(T)T

所有内存分配都是使用 的副本完成的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; }
Run Code Online (Sandbox Code Playgroud)

既然Mallocator只能分配 的内存sizeof(T),那么如何allocate_shared分配大于 的存储空间sizeof(T)以便为共享指针的控制块和T对象使用一次分配呢?

cdh*_*wie 5

这是一个由两部分组成的答案。首先我将讨论它合理地可以做什么,然后我将解释如何做。

T目标是在同一分配中分配一个控制块和空间。这可以通过内部模板结构来完成,如下所示:

template <typename T>
struct shared_ptr_allocation {
    shared_ptr_control_block cb;
    typename std::aligned_storage<sizeof(T)>::type storage;
};
Run Code Online (Sandbox Code Playgroud)

(假设存在内部shared_ptr_control_block类型。我不认为该标准要求使用任何特定结构,这只是一个示例,可能适合也可能不适合实际实现。)

因此,所需std::allocate_shared()要做的就是分配 ashared_ptr_allocation<T>并获取控制块存储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...
}
Run Code Online (Sandbox Code Playgroud)

control_block_allocator用于执行实际分配。

检查此示例,其中我们在执行分配时显示类型的名称T,然后使用std::allocate_shared分配int. 如果 的重整类型名称为inti则我们要分配的重整类型名称为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如果存在,否则Alloc<T, Args>如果这AllocAlloc<U, Args>

由于此示例类型没有rebind模板成员,因此此重新绑定模板只是将模板参数剥离Mallocator<whatever>并替换whatever为新类型(保留以下模板参数,如果有的话 - 在本例中没有)。

但是为什么要用旧的分配器构建新的分配器,以及它是如何工作的呢?这在您链接到自己的同一页面上有所介绍:

A a(b): 构造a这样的B(a)==bA(b)==a。不抛出异常。(注意:这意味着通过rebind关联的所有分配器都维护彼此的资源,例如内存池)