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:
// 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
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:
// 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 ...
Method 2 - Using new
operator
For example:
// 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 ...
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