2Brute2Best
πŸ”₯0
M6.4

Implement Your Own unique_ptr<T> and shared_ptr<T>

Hard High-frequency interview topic Pro OnlyUniquePtr<T>Deleted Copy / Move TransferSharedPtr<T>Atomic Ref-Count Control Blockrelease() vs reset()
⚑ 15-Second Mental ModelM6.4 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Core Intuition)

In modern C++, we never guess what the compiler does. We look directly at byte offsets, CPU registers, and object ownership lifetimes.

Rule to Remember: Make illegal states impossible to compile or run.
3 Golden Facts to Remember
Fact #1
Why does std::unique_ptr take an optional deleter template parameter while std::shared_ptr erases the deleter type?

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.

Fact #2
What happens if two threads simultaneously call reset() on two distinct shared_ptr instances sharing the same Control Block?

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.

Fact #3
Why is std::make_shared preferred over explicit shared_ptr<T>(new T), and when might it be disadvantageous?

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.

01

Interviewer's lens

What separates a strong C++ candidate from a textbook answer

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.

02

Under the hood & memory layout

Hardware & memory representation

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.

UniquePtr<T> (8 Bytes)

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_.

SharedPtr<T> (16 Bytes)

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.

ControlBlock (Heap Node)

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.

Atomic Acquire-Release Synchrony

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.

03

Core concepts & syntax traps

01

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).

C++23 snippet
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;
    }
};
Interview takeaway:

Always guard move assignment against self-move (this != &other) before deleting existing resources.

02

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 raw T* and resets ptr_ = nullptr. The caller assumes full responsibility for freeing that memory.
  • reset(T* p = nullptr) destroys the currently owned object and adopts p. You must guard against ptr_ != p: calling p.reset(p.get()) without this guard causes delete ptr_ to destroy the object before assigning it right back, leaving ptr_ pointing to dead memory!
C++23 snippet
[[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;
    }
}
Interview takeaway:

release() abandons ownership without deleting; reset() deletes the old resource and adopts the new pointer.

03

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) uses std::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) uses std::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 execute delete ptr_ and delete cb_.
C++23 snippet
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;
    }
}
Interview takeaway:

Use memory_order_relaxed for atomic ref increments, but memory_order_acq_rel for decrements before deletion.

04

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.

C++23 snippet
// Zero atomic operations on move!
SharedPtr(SharedPtr&& other) noexcept
    : ptr_(other.ptr_), cb_(other.cb_) {
    other.ptr_ = nullptr;
    other.cb_ = nullptr;
}
Interview takeaway:

Moving a SharedPtr executes zero atomic instructions: transfer the two pointers and null out the source.

Brute2Best Pro Content

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.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering