Skip to content

std :: shared_ptr

The shared_ptr is an object that owns another object through a pointer, usually obtained via make_shared or new operator.

The shared_ptr implements semantics of shared ownership; the last remaining owner of the pointer is responsible for destroying the object and releasing resources associated with it.

For example, we can create shared_ptr object with one of the two methods:

cpp
// 1. Create an object + acquire ownership (all in one call).
auto make_ptr = make_shared<MyClass>();

// 2. Create an object (w/o ownership yet),
auto raw_ptr = new MyClass();
// and acquire ownership.
auto new_ptr = shared_ptr<MyClass>(raw_ptr);

However, the memory layout of make_ptr and new_ptr is different. Encapsulation is a core feature of C++, and it's not a surprise it's employed here (and the entire C++ Standard Library).

Let's see at the differences in more details.

Implementation

The shared_ptr is a pair of two pointers, its size is 16 bytes, and is defined as

cpp
template<typename T>
class shared_ptr {
private:
  T*   m_ptr; // object
  Ref* m_ref; // control block
}

Layout

Let's take a look at the memory layout of shared_ptr when created using various methods.

Method 1 - Using make_shared function

For example:

cpp
// 1. Create an object + acquire ownership (all in one call).
auto make_ptr = make_shared<MyClass>();

// 2. Create an object (w/o ownership yet),
auto raw_ptr = new MyClass();
// and acquire ownership.
auto new_ptr = shared_ptr<MyClass>(raw_ptr);

As you can see ...

std::shared_ptr memory layout

Method 2 - Using new operator

For example:

cpp
// 1. Create an object + acquire ownership (all in one call).
auto make_ptr = make_shared<MyClass>();

// 2. Create an object (w/o ownership yet),
auto raw_ptr = new MyClass();
// and acquire ownership.
auto new_ptr = shared_ptr<MyClass>(raw_ptr);

As you can see ...

std::shared_ptr memory layout

In Depth

It's not that complicated to implement shared_ptr. One thing we should be careful about is when updating the reference counter, since the reference counter might be changed from multiple threads, when copying or destroying the pointer (but not the object itself).

We should ensure exclusive access to a memory region of the reference counter when multiple threads try change it. This is exactly what LOCK prefix assembly instruction does. (Available since the original Intel 8086 processor, released in 1978, it has been a foundational feature since the very beginning of the x86 architecture.)

mov eax, 1;
mov ebx, ptr;
lock add [ebx], eax

Using LOCK introduces performance penalties due to cache invalidation, pipeline stalling (no out-of-order execution), and waiting time to acquire the lock.

Before atomic operations were part of the C++ Standard, the atomic operations were done with OS-specific functions. For example, InterlockedIncrement function has been available since Windows XP (released in 2001).

See Also

https://herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers