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:
RAII: Resource Acquisition Is Initialization, when construct, acquire the resource, when destruct, release the resource.
Unique ownership: only one unique ptr “owns” the memory it points to. Meaning forbid copy, allow move.
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:
Fix the cycle reference problem of shared_ptr.
Observer pattern: It only have a pointer points to the control block
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
expired(): return true if shared_count == 0
Implementation
shared_ptr & weak_ptr & control block
So we neeed 2 parts:
control block (RefBlockBase): record ref count, weak count, and responsible for deleting the actual objecta and itselt.
#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{ structRefBlockBase { // 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 virtualvoiddispose_resource()= 0; // "destroy" = destroy control block itself (like, delete this) virtualvoiddestroy_self()= 0; // get T's pointer virtualvoid* get_resource_ptr(){ returnnullptr; }
voiddecrement_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(); } }
voiddecrement_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 booltry_increment_shared()noexcept{ size_t count = m_shared_count.load(std::memory_order_relaxed);
// if failes it will continue the loop } returnfalse; } };
// for 'new' // Y is the actual type, D is the del type template <typename Y, typename D> structRefBlockImpl : public RefBlockBase { Y* m_resource; D m_deleter;
RefBlockImpl(Y* res, D del) : m_resource(res), m_deleter(mystl::move(del)){}
voiddispose_resource()override{ // call the deleter m_deleter(m_resource); }
template <typename T> structRefBlockMakeShared : 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{ returnreinterpret_cast<T*>(m_storage); }
voiddispose_resource()override{ // call the deconstruct but not release the mmroy reinterpret_cast<T*>(m_storage) -> ~T(); }
voiddestroy_self()override{ // delete self, this will release the memory in make_shared deletethis; } };
} // 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.
template <typename T> classMyWeakPtr { 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 friendclassMySharedPtr<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(); } }
// try to atomatically increment shared count if (m_block -> try_increment_shared()){ //sucess, now we are one of the legal owners returnMySharedPtr<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 returnMySharedPtr<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 = newRefBlockMakeShared<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)...);