Implement Your Own unique_ptr<T> and shared_ptr<T>
In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.
In std::unique_ptr<T, Deleter>, the deleter is part of the class type so that stateless deleters occupy zero bytes via the Empty Base Optimization (EBO) or [[no_unique_address]], maintaining sizeof(unique_ptr) == sizeof(T*) == 8 bytes with direct inlined deletion. In std::shared_ptr<T>, the deleter is type-erased inside the heap Control Block. This allows std::shared_ptr<Base> to hold pointers to derived types or custom allocators without changing the smart pointer's signature.
It is completely thread-safe. Both threads execute cb_->strong_count.fetch_sub(1, std::memory_order_acq_rel). The hardware atomic unit serializes the operations: exactly one thread receives a return value of 1. That winning thread becomes the sole owner responsible for running delete ptr_ and delete cb_, completely preventing double-free bugs.
std::make_shared performs 1 single heap allocation combining the ControlBlock and T in adjacent bytes, improving CPU cache locality and eliminating one heap allocation overhead. The disadvantage occurs with std::weak_ptr: because T and ControlBlock share a single chunk, the heap memory for T cannot be freed back to the OS until all weak_ptr instances expire, even though ~T() has already executed.
Interviewer's lens
Asking candidates to implement unique_ptr and shared_ptr from scratch is one of the highest-signal coding exercises in modern C++ interviews because it ruthlessly exposes whether you understand RAII ownership contracts, Rule of 5 resource transfers, and atomic memory ordering. Weak candidates write a naive reference counter with a raw int (data race!) or forget self-assignment checks, double-freeing their own memory on ptr = std::move(ptr). Senior candidates immediately specify noexcept move constructors with zero atomic operations, guard against p.reset(p.get()) self-destruction, separate the strong count from the weak count in a heap Control Block, and write thread-safe CAS (compare_exchange_weak) loops in WeakPtr::lock() to prevent resurrecting dead objects.
Under the hood & memory layout
Under the hood, UniquePtr<T> is a zero-cost abstraction storing exactly one 8-byte raw pointer (T* ptr_), with copy operations deleted at compile time and move operations transferring the address while zeroing the source. SharedPtr<T> occupies 16 bytes on 64-bit architectures: an 8-byte pointer to the managed object (T* ptr_) and an 8-byte pointer to a heap-allocated ControlBlock* cb_. The ControlBlock contains two atomic words: std::atomic<size_t> strong_count and std::atomic<size_t> weak_count. Copying a SharedPtr issues an atomic increment (fetch_add(1, std::memory_order_relaxed)). Moving a SharedPtr simply steals both 8-byte pointers and nulls the sourceβexecuting zero atomic bus instructions. When strong_count drops to 0 via fetch_sub(1, std::memory_order_acq_rel), the managed object T is destroyed immediately; the ControlBlock itself remains alive until weak_count also reaches 0.
Holds a single raw pointer T* ptr_ (8 bytes on 64-bit). No heap allocation overhead, no atomic locks. Move assignment cleans up existing ptr_ before stealing other.ptr_.
Pair of two 8-byte pointers: T* ptr_ (payload address) and ControlBlock* cb_ (metadata address). Moving a SharedPtr copies two registers and clears the source without atomic pipeline stalls.
Contains std::atomic<size_t> strong_count (active owners) and std::atomic<size_t> weak_count (observers + 1 for strong group). Decouples object lifetime from metadata lifetime.
Decrements use std::memory_order_acq_rel. This ensures all writes to *ptr_ made by any thread release-synchronize with the thread whose decrement observes count 1 and acquires destruction rights.
Core concepts & syntax traps
UniquePtr<T>: Deleted Copy, noexcept Move Transfer & The Self-Move Bug
UniquePtr<T>: Deleted Copy, noexcept Move Transfer & The Self-Move Bug
A custom UniquePtr<T> encapsulates a single raw address T* ptr_. To enforce exclusive ownership, both the copy constructor and copy assignment operator must be marked = delete. The move constructor simply steals other.ptr_ and writes other.ptr_ = nullptr.
The move assignment operator requires special care: writing delete ptr_; ptr_ = other.ptr_; other.ptr_ = nullptr; without checking this != &other is catastrophic! In code like p = std::move(p);, this and &other alias the same object. The naive code deletes ptr_, then reads the newly deleted address from other.ptr_ and immediately nulls it, causing a use-after-free or dangling null pointer. Always guard with if (this != &other).
template <typename T>
class UniquePtr {
T* ptr_{nullptr};
public:
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
UniquePtr(UniquePtr&& other) noexcept : ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete ptr_; // Delete existing owned object
ptr_ = other.ptr_; // Steal new address
other.ptr_ = nullptr; // Nullify source
}
return *this;
}
};Always guard move assignment against self-move (this != &other) before deleting existing resources.
release() vs reset(): The Ownership Hand-off Contract
release() vs reset(): The Ownership Hand-off Contract
Interviewers frequently test whether you know the difference between release() and reset():
release()relinquishes ownership without destroying the managed object. It returns the rawT*and resetsptr_ = nullptr. The caller assumes full responsibility for freeing that memory.reset(T* p = nullptr)destroys the currently owned object and adoptsp. You must guard againstptr_ != p: callingp.reset(p.get())without this guard causesdelete ptr_to destroy the object before assigning it right back, leavingptr_pointing to dead memory!
[[nodiscard]] T* release() noexcept {
T* old = ptr_;
ptr_ = nullptr;
return old; // Caller now owns memory!
}
void reset(T* p = nullptr) noexcept {
if (ptr_ != p) { // Guard against p.reset(p.get())!
delete ptr_;
ptr_ = p;
}
}release() abandons ownership without deleting; reset() deletes the old resource and adopts the new pointer.
SharedPtr<T>: Atomic Strong Count & Memory Ordering Guarantees
SharedPtr<T>: Atomic Strong Count & Memory Ordering Guarantees
A naive reference count using size_t count is a critical bug: two threads copying or destroying SharedPtr instances concurrently will race on ++count or --count, corrupting the counter and triggering double-free crashes. Modern C++ requires std::atomic<size_t> in the heap ControlBlock.
Memory ordering selection is crucial for maximum performance:
- Copying (
fetch_add) usesstd::memory_order_relaxed: No synchronization of other memory writes is needed when creating a new shared owner because the target object already exists. - Destruction (
fetch_sub) usesstd::memory_order_acq_rel: All modifications made to*ptr_across all threads must synchronize before the thread that decrements the count to 1 proceeds to executedelete ptr_anddelete cb_.
void release_ref() noexcept {
if (cb_) {
// acq_rel ensures memory visibility before destruction
if (cb_->strong_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete ptr_;
delete cb_;
}
ptr_ = nullptr;
cb_ = nullptr;
}
}Use memory_order_relaxed for atomic ref increments, but memory_order_acq_rel for decrements before deletion.
The Zero-Atomic Move Optimization in SharedPtr<T>
The Zero-Atomic Move Optimization in SharedPtr<T>
Atomic read-modify-write instructions (lock xadd on x86) lock the CPU cache line and stall processor pipelines. A common interview mistake when implementing SharedPtr(SharedPtr&& other) is calling the copy constructor or incrementing the atomic count.
Because moving transfers an existing ownership ticket from other to this, the total number of owners remains strictly identical! The move constructor only needs to copy the two 8-byte pointer registers (ptr_ = other.ptr_, cb_ = other.cb_) and clear other.ptr_ = other.cb_ = nullptr. This achieves zero atomic operations, making moving a SharedPtr nearly as cheap as moving a raw pointer.
// Zero atomic operations on move!
SharedPtr(SharedPtr&& other) noexcept
: ptr_(other.ptr_), cb_(other.cb_) {
other.ptr_ = nullptr;
other.cb_ = nullptr;
}Moving a SharedPtr executes zero atomic instructions: transfer the two pointers and null out the source.
Unlock Live Implementation & Interview Scenarios
The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.