alpha
alpha

Reputation: 1298

How allocate_shared works?

From std::allocate_shared in cppreference:

Constructs an object of type T and wraps it in a std::shared_ptr using args as the parameter list for the constructor of T. The object is constructed as if by the expression ::new (pv) T(std::forward<Args>(args)...), where pv is an internal void* pointer to storage suitable to hold an object of type T. The storage is typically larger than sizeof(T) in order to use one allocation for both the control block of the shared pointer and the T object.

All memory allocation is done using a copy of alloc, which must satisfy the Allocator requirements.

What make me confused is, think about following user-defined allocator, also from 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; }

Since Mallocator can only allocate memory of sizeof(T), how could allocate_shared allocate a storage which larger than sizeof(T) in order to use one allocation for both the control block of the shared pointer and the T object?

Upvotes: 4

Views: 2968

Answers (1)

cdhowie
cdhowie

Reputation: 169018

This is a two part answer. First I will address what it reasonably could do, then I will explain how.

The goal is to allocate a control block and the room for a T within the same allocation. This can be done with an internal template struct, like this:

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

(Assuming the presence of an internal shared_ptr_control_block type. I don't believe the standard requires any specific structure to be used, this is just an example and may or may not be suitable for an actual implementation.)

So all std::allocate_shared() needs to do is allocate a shared_ptr_allocation<T> and it gets both the control block and the T storage, which will be initialized with a placement-new later.


But how do we obtain an allocator suitable to allocate this struct? This, I believe is the crux of your question, and the answer is quite simple: std::allocator_traits.

This trait has a rebind_alloc template member, which can be used to obtain an allocator of a different type, which is constructed from your own allocator. For example, the first few lines of allocate_shared might look like:

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...
}

And control_block_allocator is used to perform the actual allocation.

Check this sample where we display the name of the T type when an allocation is performed, and then use std::allocate_shared to allocate an int. Where the mangled type name of int is i, the mangled type name of what we're allocating is St23_Sp_counted_ptr_inplaceIi10MallocatorIiELN9__gnu_cxx12_Lock_policyE2EE. Clearly we're allocating something different!


Aside: we can confirm this by forward-declaring an allocator template and specializing it for one type, basically leaving the other specializations without a definition and therefore incomplete. The compiler should throw a fit when we try to allocate_shared with that allocator, and behold, it does!

error: implicit instantiation of undefined template 'OnlyIntAllocator<std::_Sp_counted_ptr_inplace<int, OnlyIntAllocator<int>, __gnu_cxx::_Lock_policy::_S_atomic> >'

So, in this implementation, std::_Sp_counted_ptr_inplace is the template struct that holds both the control block and the object storage.


Now, to address how rebinding actually manages to work in practice, there's two key requirements here and this really simple allocator meets both of them. First, we need std::allocator_traits<...>::rebind_other<...> to actually work. From the docs cppreference:

rebind_alloc<T>: Alloc::rebind<T>::other if present, otherwise Alloc<T, Args> if this Alloc is Alloc<U, Args>

Since this example type doesn't have a rebind template member, this rebind template simply strips the template arguments off of Mallocator<whatever> and replaces whatever with the new type (preserving the following template arguments, if any -- in this case there are none).

But why construct the new allocator with the old, and how is that expected to work? That's covered on the same page you linked to yourself:

A a(b): Constructs a such that B(a)==b and A(b)==a. Does not throw exceptions. (Note: this implies that all allocators related by rebind maintain each other's resources, such as memory pools)

Upvotes: 5

Related Questions