Complete code can be foound here: (https://github.com/Interstellarss/mystl/tree/main/include/ptr)

Smart Pointer

Smart pointers are a type of pointer that automatically manage the memory of the object they point to. They are used to prevent memory leaks and other memory-related errors.

There are three types of smart pointers:

  • std::unique_ptr
  • std::shared_ptr
  • std::weak_ptr

unique_ptr

unique_ptr is a smart pointer that owns and manages another object through a pointer and disposes of that object when the unique_ptr goes out of scope. And at anytime, only one unique ptr “owns” the memory it points to.

Core concept:

  1. RAII: Resource Acquisition Is Initialization, when construct, acquire the resource, when destruct, release the resource.
  2. Unique ownership: only one unique ptr “owns” the memory it points to. Meaning forbid copy, allow move.
  3. Move semantics: when move, transfer the ownership from the source to the destination, and the old pointer is null.

shared_ptr

shared_ptr is a smart pointer that owns and manages another object through a pointer and disposes of that object when the last shared_ptr owning the object goes out of scope. So basically shared ownership is implemented here.

shared_ptr is a copyable type, meaning it can be copied, but it cannot be moved.

Core components:

  • control block, which should contain at least:

    • shared_count: the number of shared_ptr instances that own the object
    • weak_count: the number of weak_ptr instances that point to the object
    • resource_ptr: the pointer to the object
  • copy: copy the control block, increment the shared_count

  • destructor: decrement the shared_count, if it reaches 0, delete the control block and the resource

  • release: decrement the shared_count, if it reaches 0, delete the resource. when both shared_count and weak_count reach 0, delete the control block.

weak_ptr

weak_ptr is a non-owning observer of a shared object. It does not prevent the object from being destroyed.

Core Concept:

  1. Fix the cycle reference problem of shared_ptr.
  2. Observer pattern: It only have a pointer points to the control block
  3. lock(): weak_ptr can not directly access the resource, it should use lock() to check if still exists (shared_count > 0).
  • if exists, return a new shared_ptr to the resource, and shared_count++
  • if not exists, return a null shared_ptr
  1. expired(): return true if shared_count == 0

Implementation

shared_ptr & weak_ptr & control block

So we neeed 2 parts:

  1. control block (RefBlockBase): record ref count, weak count, and responsible for deleting the actual objecta and itselt.
  2. actual objects (T)
1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────┐
│ RefBlockBase │ ← 控制块(记录引用数)
│ ├─ shared_count │
│ ├─ weak_count │
│ ├─ dispose_resource │ → delete 或 调用析构
│ ├─ destroy_self │ → delete this
└──────────┬──────────┘


┌──────────────┐
│ 对象 T │ ← 实际资源
└──────────────┘

For RefBlockBase:

in standard library, make_shared allocate once with control block + the object memory, and this is also how it is implemented here.

For thread safe, we use atomic operation for both shared_count and weak_count, and use acq_rel memory order to ensure memory safe.

Let’s imagine two threads:

Thread A Thread B
Uses shared_ptr Destroys last shared_ptr
Reads object data Calls fetch_sub(…, acq_rel) and deletes the object

Because of acq_rel, Thread A’s reads happen-before the destruction in Thread B.
No read-after-free will occur.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#ifndef REF_BLOCK_BASE_H
#define REF_BLOCK_BASE_H

#include <atomic>
#include "my_utility.h"
#include <memory>
#include <functional>
#include <cstddef> // for std::size_t, std::alignas

// base control class (with atomic operation and type erasure)

namespace mystl{
struct RefBlockBase {
// atomic operation
std::atomic<size_t> m_shared_count{1}; // when init, create the shared_ptr and own it
std::atomic<size_t> m_weak_count{1}; // when init, m_shared_count as one "weak reference"

virtual ~RefBlockBase() = default;

// type erasure
// "dispose" = destroy resource
virtual void dispose_resource() = 0;
// "destroy" = destroy control block itself (like, delete this)
virtual void destroy_self() = 0;
// get T's pointer
virtual void* get_resource_ptr() { return nullptr; }


// ---- thread safe counter operation ----
void increment_shared() noexcept{
m_shared_count.fetch_add(1, std::memory_order_relaxed);
}

void increment_weak() noexcept{
m_weak_count.fetch_add(1, std::memory_order_relaxed);
}

void decrement_shared() noexcept {
// fetch_sub return the value before minus
// use acq_rel (Acquire-Release) ensure memory safe
if(m_shared_count.fetch_sub(1, std::memory_order_acq_rel) == 1){
// if shared counter goes 0, destroy resource
dispose_resource();
// when a shared ptr destroyed, the weak ref it has is also gond
decrement_weak();
}
}

void decrement_weak() noexcept{
if (m_weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1){
// weak counter oges 0, destroy control block
destroy_self();
}
}

// try lock if shared_coutn > 0, so it atomically adds by 1, and return true
bool try_increment_shared() noexcept {
size_t count = m_shared_count.load(std::memory_order_relaxed);

while(count != 0){
// try to replace count with count + 1
if(m_shared_count.compare_exchange_weak(count, count + 1, std::memory_order_acq_rel)){
return true; // success
}

// if failes it will continue the loop
}
return false;
}
};

// for 'new'
// Y is the actual type, D is the del type
template <typename Y, typename D>
struct RefBlockImpl : public RefBlockBase {
Y* m_resource;
D m_deleter;

RefBlockImpl(Y* res, D del)
: m_resource(res), m_deleter(mystl::move(del)){}

void dispose_resource() override {
// call the deleter
m_deleter(m_resource);
}

void destroy_self() override {
// destroy self
delete this;
}
};

template <typename T>
struct RefBlockMakeShared : public RefBlockBase{
// T's data will followed directly after this struct
// use an aligned char array for padding
alignas(T) char m_storage[sizeof(T)];

// get T's pointer
void* get_resource_ptr() override{
return reinterpret_cast<T*>(m_storage);
}

void dispose_resource() override {
// call the deconstruct but not release the mmroy
reinterpret_cast<T*>(m_storage) -> ~T();
}

void destroy_self() override{
// delete self, this will release the memory in make_shared
delete this;
}
};

} // namespace mystl
#endif REF_BLOCK_BASE_H

For shared ptr and weak ptr:

What is the job of weak_ptr?

weak_ptr is an observer — it points to the same control block as a shared_ptr,
but does not own (i.e. keep alive) the managed object.

So:

shared_ptr → “I own the object.”

weak_ptr → “I know about the object, but don’t keep it alive.”

When all shared_ptrs are gone, the object is destroyed — but the control block (with counters) stays alive as long as weak_ptrs exist.

The problem without lock()

Let’s imagine weak_ptr directly exposed the raw pointer to the object:

T* ptr = weak_ptr->get(); // (if it existed)

But — between that line and your first use of ptr,
another thread might destroy the last shared_ptr, leading to:

Thread A: uses weak_ptr, gets raw pointer
Thread B: destroys last shared_ptr -> deletes the object
Thread A: dereferences raw pointer -> 💥 undefined behavior

So a simple get() is unsafe, because the object might be destroyed at any moment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#ifndef MY_PTR_H
#define MY_PTR_H

#include "ref_block_base.h"
#include "my_utility.h"
#include <type_traits>

namespace mystl {

// forwared declaration
template <typename T>
class MySharedPtr;

template <typename T>
class MyWeakPtr;

template <typename T, typename... Args>
MySharedPtr<T> MyMakeShared(Args&&... args);

template <typename T>
class MySharedPtr {
// allow myweaktptr to access prive
friend class MyWeakPtr<T>;
template<typename U, typename... Args>
friend MySharedPtr<U> MyMakeShared(Args&&... args);

private:
T* m_ptr = nullptr;

RefBlockBase* m_block = nullptr;

//private construck for myweakptr lock and MyMakeShared
MySharedPtr(T* ptr, RefBlockBase* block) noexcept
: m_ptr(ptr), m_block(block) {}

public:
// --- construct func ---

// default
MySharedPtr() noexcept = default;

// accept rao ptr
explicit MySharedPtr(T* ptr)
: MySharedPtr(ptr, std::default_delete<T>()){

}

// construct from nullptr
MySharedPtr(std::nullptr_t) noexcept
: MySharedPtr(){}

// core construct with deleter
template <typename Y, typename D,
typename = typename std::enable_if<
!std::is_convertible<typename std::decay<D>::type, RefBlockBase*>::value>::type>
MySharedPtr(Y* ptr, D&& deleter)
: m_ptr(ptr){
using DeleterType = typename std::decay<D>::type;
DeleterType deleter_copy = mystl::forward<D>(deleter);
try{
// try to allocate for control block
m_block = new RefBlockImpl<Y, DeleterType>(ptr, deleter_copy);
}catch(...){
// if new refblockimpl fails, delete ptr and through error
deleter_copy(ptr);
throw;
}
}

// --- deconstructor ---
~MySharedPtr() noexcept {
if (m_block){
m_block->decrement_shared();
}
}

// --- copy control ---
MySharedPtr(const MySharedPtr& other) noexcept
: m_ptr(other.m_ptr), m_block(other.m_block){
if(m_block){
m_block -> increment_shared();
}
}

MySharedPtr& operator=(const MySharedPtr& other) noexcept{
if(this != &other){
// 1. release old resource
if(m_block){
m_block -> decrement_shared();
}

// 2. copy new resource
m_ptr = other.m_ptr;
m_block = other.m_block;

if(m_block){
m_block->increment_shared();
}
}
return *this;
}

MySharedPtr& operator=(std::nullptr_t) noexcept{
if(m_block){
m_block->decrement_shared();
m_block = nullptr;
m_ptr = nullptr;
}
return *this;
}

// --- accesser ---
T* get() const noexcept {return m_ptr;}

T& operator*() const noexcept {return *m_ptr;}
T* operator->() const noexcept {return m_ptr;}

size_t use_count() const noexcept{
return m_block ? m_block->m_shared_count.load() : 0;
}

explicit operator bool() const noexcept {return m_ptr != nullptr;}

};


template <typename T>
class MyWeakPtr {
private:
// weak ptr can not safely holding T*, cause T can be destroyed any time
RefBlockBase* m_block = nullptr;

public:
// allow shared ptr to access
friend class MySharedPtr<T>;

MyWeakPtr() noexcept = default;

// construct from my shared ptr
MyWeakPtr(const MySharedPtr<T>& shared) noexcept
: m_block(shared.m_block){
if (m_block){
m_block->increment_weak();
}
}

~MyWeakPtr() noexcept {
if(m_block){
m_block -> decrement_weak();
}
}

bool expired() const noexcept {
return !m_block || m_block -> m_shared_count.load(std::memory_order_acquire) == 0;
}

// core utility lock()
MySharedPtr<T> lock() const noexcept{
if(expired()){
return MySharedPtr<T>();
}

// try to atomatically increment shared count
if (m_block -> try_increment_shared()){
//sucess, now we are one of the legal owners
return MySharedPtr<T>(reinterpret_cast<T*>(m_block->get_resource_ptr()), m_block);

}else{
// if counter turn to zero just at the time we try to check and access
return MySharedPtr<T>();
}
}
};

template <typename T, typename... Args>
MySharedPtr<T> MyMakeShared(Args&&... args){
// allocate one time for space for T and RefBlock
// allocate refblockmakeshared, which includes already size for T
RefBlockMakeShared<T>* block = new RefBlockMakeShared<T>();

// get pointer to T
void* void_ptr = block->get_resource_ptr();
T* ptr = static_cast<T*>(void_ptr);

// placement new to construct T on this address
try {
::new (ptr) T(mystl::forward<Args>(args)...);

} catch (...){
// fail to construct
delete block;
throw;
}

// use private constructor to return mysharedptr
// now the RefBlockBase's m
return MySharedPtr<T>(ptr, block);
}
} // namespace mystl
#endif

unique_ptr

bascially just ban copy, allow move

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#ifndef MY_UNIQUE_PTR
#define MY_UNIQUE_PTR


#include <iostream>
#include "my_utility.h"

namespace mystl{

template <typename T>
class MyUniquePtr{
private:
T* m_ptr;

public:
// construct func
explicit MyUniquePtr(T* ptr = nullptr) noexcept
: m_ptr(ptr){}

// deconstruct func: release resource
~MyUniquePtr() noexcept{
delete m_ptr;
}

//---- ban copy ----
MyUniquePtr(const MyUniquePtr& other) = delete;
//---- ban copy operator ----
MyUniquePtr& operator=(const MyUniquePtr& other) = delete;

//---- enable move ---
MyUniquePtr(MyUniquePtr&& other) noexcept
: m_ptr(other.m_ptr){
// "steal" the old ptr
other.m_ptr = nullptr;
}

// Move operator
MyUniquePtr& operator=(MyUniquePtr&& other) noexcept{
if(this != &other){
// 1. release the resource current holding
delete m_ptr;

//2. steal other resource
m_ptr = other.m_ptr;

// 3. nullptr of other;
other.m_ptr = nullptr;
}
return *this;
}

// behavior as pointer
T& operator*() const noexcept {return *m_ptr;}
T* operator->() const noexcept {return m_ptr;}

// check if holding pointer
explicit operator bool() const noexcept {
return m_ptr != nullptr;
}

// access to the pointer
T* get() const noexcept{
return m_ptr;
}

// release ownership and return pointer
T* release() noexcept{
T* temp = m_ptr;
m_ptr = nullptr;
return temp;
}

//reset
void reset(T* ptr = nullptr) noexcept{
delete m_ptr;
m_ptr = ptr;
}

};
} // namespace mystl
#endif // MY_UNIQUE_PTR