Reputation: 1298
From std::allocate_shared in cppreference:
Constructs an object of type
T
and wraps it in astd::shared_ptr
usingargs
as the parameter list for the constructor ofT
. The object is constructed as if by the expression::new (pv) T(std::forward<Args>(args)...)
, wherepv
is an internalvoid*
pointer to storage suitable to hold an object of typeT
. The storage is typically larger thansizeof(T)
in order to use one allocation for both the control block of the shared pointer and theT
object.All memory allocation is done using a copy of
alloc
, which must satisfy theAllocator
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
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, otherwiseAlloc<T, Args>
if thisAlloc
isAlloc<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)
: Constructsa
such thatB(a)==b
andA(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