How std::make_shared Allocates Memory in C++
This article explores the internal mechanics of
std::make_shared in the C++ Standard Library, focusing on
how it consolidates heap allocations. While creating a
std::shared_ptr directly from a raw pointer requires two
distinct heap allocations, std::make_shared optimizes this
process by allocating the managed object and its associated control
block inside a single contiguous memory segment. The following sections
detail this memory layout, the construction mechanism, and the resulting
performance implications.
The Standard Allocation Problem
When you initialize a std::shared_ptr using a raw
pointer, such as:
std::shared_ptr<Widget> ptr(new Widget());Two separate dynamic memory allocations take place:
- The
new Widget()expression invokesoperator newto allocate memory specifically for theWidgetinstance. - The
std::shared_ptrconstructor invokesoperator newa second time to allocate an internal control block. This control block maintains the strong reference count, the weak reference count, and auxiliary data such as custom deleters or allocators.
This dual-allocation pattern introduces heap fragmentation, doubles allocator overhead, and harms cache locality because the control block and the target object reside in unrelated memory addresses.
The
std::make_shared Contiguous Architecture
std::make_shared resolves the dual-allocation issue by
combining the control block and the managed object into a unified data
structure.
Internally, standard library implementations define a combined internal structure conceptually structured as follows:
template <typename T>
struct SpControlBlockWithStorage {
// Standard control block members
std::atomic<long> strong_ref_count;
std::atomic<long> weak_ref_count;
// ... other control metadata ...
// Properly aligned storage for the managed object
alignas(T) unsigned char storage[sizeof(T)];
};Instead of performing two separate allocations,
std::make_shared calculates the total size needed for both
the control metadata and the object type T, accounting for
proper memory alignment. It then issues a single call to the
allocator:
void* memory = ::operator new(sizeof(SpControlBlockWithStorage<T>));In-Place Construction via Placement New
Once the single contiguous block of raw memory is obtained,
std::make_shared initializes the components
sequentially:
- Control Block Initialization: The control block
members (such as
strong_ref_countinitialized to1andweak_ref_countinitialized to1) are set up at the beginning of the memory block. - Object Construction: The user-provided arguments
are forwarded to the constructor of
Tusing perfect forwarding (std::forward<Args>(args)...). The object is constructed directly inside the designated offset of the contiguous memory block via placementnew:
::new (static_cast<void*>(storage_address)) T(std::forward<Args>(args)...);Finally, the std::shared_ptr instance is constructed by
setting its internal pointer to point directly to the newly constructed
T and its internal control block pointer to the base of the
allocated segment.
Advantages of Contiguous Allocation
- Halved Allocation Overhead: Performing one heap allocation instead of two significantly reduces dynamic memory management latency and reduces heap bookkeeping overhead.
- Improved Cache Locality: Because the reference counts and the object data reside adjacent to one another in memory, accessing the object immediately after manipulating reference counts is more likely to result in a CPU cache hit.
- Strict Exception Safety: Constructing the object and smart pointer in one unified function prevents memory leaks that could occur if an exception was thrown between raw memory allocation and smart pointer construction.
The Weak Pointer Trade-Off
While contiguous allocation offers clear performance benefits, it
introduces a trade-off regarding memory deallocation when
std::weak_ptr is used.
In a traditional two-allocation setup, the object's memory is freed
as soon as the strong reference count drops to zero, even if weak
pointers still point to the control block. With
std::make_shared, because the object and control block
occupy the same contiguous block of memory, the underlying memory cannot
be released back to the heap until both the strong
reference count and the weak reference count reach zero. The destructor
of T runs immediately when the strong count hits zero, but
the memory footprint of sizeof(T) remains allocated until
all associated std::weak_ptr instances are destroyed.