2Brute2Best
🔥0
M1.6

Smart Pointers (unique_ptr, shared_ptr, weak_ptr)

Medium High-frequency interview topicunique_ptrshared_ptr Control Blockmake_sharedweak_ptr CyclesCustom Deleters
⚡ 15-Second Mental ModelM1.6 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Smart Pointers & Ownership)

std::unique_ptr is a digital passport (strictly 1 owner; if you hand it over, your own hands are empty). std::shared_ptr is a Netflix account with a viewer counter (plays as long as counter > 0; turns off the TV when the last person logs out).

Rule to Remember: Default to std::unique_ptr with std::make_unique. Zero memory overhead.
3 Golden Facts to Remember
Fact #1
What is the memory size of std::unique_ptr and std::shared_ptr on a 64-bit architecture?

A std::unique_ptr<T> with a default or stateless deleter is 8 bytes (identical to a raw pointer T*). A std::shared_ptr<T> is 16 bytes because it must store two pointers: one pointing to the managed object T*, and one pointing to the heap Control Block.

Fact #2
Why is std::make_shared preferred over std::shared_ptr<T>(new T)? Are there any drawbacks?

Advantages: (1) std::make_shared performs 1 single heap allocation instead of 2, saving allocator metadata overhead and improving CPU cache locality, and (2) it guarantees exception safety. Drawbacks: (1) It cannot accept a custom deleter, and (2) because the Control Block and object reside in the same memory block, if any std::weak_ptr instances remain alive after the object's destructor runs, the entire memory block cannot be freed until all weak_ptr instances are destroyed.

Fact #3
Is std::shared_ptr thread-safe in C++?

The Control Block reference counter is thread-safe: multiple threads can read, copy, and destroy independent shared_ptr instances pointing to the same object concurrently without data races because operations on strong_count and weak_count use atomic operations. However, concurrent access to the managed object itself is NOT thread-safe (requires external synchronization like a mutex), and modifying the same shared_ptr instance across threads simultaneously requires std::atomic<std::shared_ptr<T>> (C++20).

01

Interviewer's lens

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

Interviewers use smart pointers to separate developers who write high-level glue code from systems engineers who understand ownership semantics, atomic costs, and heap layouts:

  • The Amateur View: Repeating that 'unique_ptr is for single owners and shared_ptr does reference counting.'
  • The Senior View: Knowing that unique_ptr is a zero-cost wrapper around an 8-byte pointer, while shared_ptr manages a complex heap Control Block using multi-core atomic operations.
  • What Evaluators Test:
    1. Zero-Overhead Proof: Why sizeof(std::unique_ptr<T>) == 8 adds zero runtime cost via the Empty Base Optimization (EBO).
    2. Control Block Deep Dive: How std::make_shared performs 1 single contiguous heap allocation versus 2 separate allocations for shared_ptr<T>(new T), and the weak-reference memory retention trade-off.
    3. Cycle Breaking & Thread Safety: Why cyclic shared_ptr graphs leak memory permanently, how weak_ptr::lock() atomizes access, and which operations are thread-safe.
02

Under the hood & memory layout

Hardware & memory representation

Think of smart pointers as contracts dictating who is responsible for turning off the lights and locking up:

  • std::unique_ptr<T> (Solo Tenant): You are the only person with the house key. When you leave, the building is demolished. Transferring the key is an instant hand-off (move); duplicating the key (copy) is illegal.
  • std::shared_ptr<T> (Community Keyring): Multiple roommates hold keys. A central ledger (the Control Block) counts active keyholders. The last person to walk out tears the building down.

Under the hood on a 64-bit system:

  1. std::unique_ptr (8 Bytes): Holds a single raw pointer T*. With a stateless deleter, the Empty Base Optimization (EBO) reduces the deleter to 0 bytes—yielding zero memory overhead and zero runtime penalty.
  2. std::shared_ptr (16 Bytes): Holds two 8-byte pointers: one to the object T*, and one to the heap Control Block.
  3. The Control Block Lifecycle: Houses atomic strong and weak counters, custom deleters, and allocators:
    • Copying a shared_ptr executes an atomic CPU instruction (lock xadd), incurring pipeline bus penalties.
    • When the strong count drops to 0, ~T() executes immediately.
    • When the weak count also drops to 0, the Control Block memory is released back to the heap.
std::unique_ptr<T> (8 Bytes)

Stores a single 8-byte pointer to T. Move-only (delete copy ctor/assignment). When using a stateless functor deleter, sizeof(unique_ptr) is strictly 8 bytes—zero CPU or RAM overhead compared to a raw pointer.

std::shared_ptr<T> (16 Bytes)

Stores two 8-byte pointers: T* ptr (points to target object) and ControlBlock* cb (points to the shared reference counting structure).

The Control Block Structure

Dynamically allocated on the heap. Houses std::atomic<uint32_t> strong_count, std::atomic<uint32_t> weak_count, type-erased custom deleter, and allocator.

std::make_shared Single Allocation

Performs 1 single heap allocation combining the Control Block and object T in contiguous memory. Direct std::shared_ptr<T>(new T) performs 2 separate heap allocations (new T + new ControlBlock).

03

Core concepts & syntax traps

01

Exclusive Ownership: std::unique_ptr & Zero-Overhead Invariant

A std::unique_ptr<T> represents single, unquestioned ownership over a resource:

  1. Move-Only Contract: Copy constructor and copy assignment are explicitly deleted (= delete). Ownership transfers strictly through std::move(), which hands over the raw pointer and sets the source variable to nullptr.
  2. Zero Memory Overhead: On 64-bit platforms, sizeof(std::unique_ptr<T>) == 8 bytes—identical to a raw pointer.
  3. The Empty Base Optimization (EBO): Even when you supply a custom deleter struct, if the deleter has no state/member variables, the compiler applies EBO (or [[no_unique_address]]) to eliminate any byte overhead.
  4. Deterministic Destruction: The instant the owning unique_ptr falls out of scope, it invokes the deleter and reclaims the resource with zero runtime indirection.
C++23 snippet
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1; // COMPILE ERROR: Call to deleted copy constructor!
std::unique_ptr<int> p2 = std::move(p1); // Transfers ownership: p1 is now nullptr, p2 owns 42
static_assert(sizeof(p2) == sizeof(int*)); // Exactly 8 bytes!
Interview takeaway:

Default to std::unique_ptr for all dynamic allocations; it provides full RAII safety with zero memory or runtime overhead.

02

Shared Ownership: std::shared_ptr & The Control Block

When an object's lifespan must extend until the last of several independent components finishes, use std::shared_ptr<T>:

  1. 16-Byte Footprint: Every shared_ptr stores two 8-byte pointers:
    • A pointer to the managed object T*.
    • A pointer to the shared heap Control Block.
  2. Atomic Reference Counting:
    • Copying a shared_ptr invokes std::atomic::fetch_add on strong_count. This CPU bus lock prevents multi-threaded race conditions but adds latency.
    • Destroying a shared_ptr decrements strong_count. When it hits 0, the destructor ~T() is executed.
  3. Pass-By-Reference Best Practice: Always pass shared pointers as const std::shared_ptr<T>& when a function merely reads the object. This avoids unnecessary atomic increment/decrement instructions.
C++23 snippet
std::shared_ptr<int> sp1 = std::make_shared<int>(100);
{
    std::shared_ptr<int> sp2 = sp1; // Atomic increment of strong_count to 2
    std::println("use_count = {}", sp1.use_count()); // 2
} // sp2 destroyed: atomic decrement of strong_count to 1
std::println("use_count = {}", sp1.use_count()); // 1
Interview takeaway:

Pass shared_ptr by const reference (const std::shared_ptr<T>&) to avoid costly atomic reference counter increments.

03

std::make_shared vs std::shared_ptr<T>(new T): The 1-Heap vs 2-Heap Allocation Trade-off

How you construct a std::shared_ptr drastically alters memory allocation and cache locality:

  1. The 2-Allocation Approach (shared_ptr<T>(new T)):
    • Allocation 1: new T allocates memory on the heap for the object.
    • Allocation 2: The shared_ptr constructor calls malloc again to create the separate Control Block.
    • Downsides: Two allocator roundtrips, fragmented memory addresses, and poor cache locality.
  2. The 1-Allocation Approach (std::make_shared<T>):
    • Performs a single contiguous heap allocation housing both the Control Block and the object T back-to-back.
    • Benefits: Halves allocator overhead and ensures the object and control block share the same cache line.
  3. The Weak Pointer Memory Retention Caveat: Because both reside in one unified memory block, the underlying RAM cannot be returned to the OS until both strong_count AND weak_count hit 0. If a weak_ptr outlives the object, memory remains allocated even after ~T() runs.
C++23 snippet
// 2 separate heap allocations (slower, poor cache locality):
std::shared_ptr<Widget> pOld(new Widget());

// 1 single contiguous heap allocation (faster, cache friendly, exception-safe):
std::shared_ptr<Widget> pFast = std::make_shared<Widget>();
Interview takeaway:

Always use std::make_shared unless you require a custom deleter or have long-lived weak_ptr instances holding large objects in memory.

04

Non-Owning Observers: std::weak_ptr & Breaking Cyclic Reference Leaks

When objects point back and forth at one another, shared_ptr creates fatal circular leaks:

  1. The Cyclic Deadlock: If Object A holds a shared_ptr to Object B, and B holds a shared_ptr to A, their strong reference counts can never drop below 1. When all outside references go away, neither destructor runs, causing a permanent memory leak.
  2. The Observer Role (std::weak_ptr<T>): A weak_ptr observes an object by pointing to its Control Block, incrementing only weak_count. It exerts no ownership and does not prevent ~T() from running.
  3. Safe Access via lock(): Because a weak pointer doesn't keep the object alive, direct dereferencing (*wp) is illegal. You must call wp.lock(), which atomically checks strong_count and returns a valid shared_ptr<T>—or nullptr if the object has already been destroyed.
C++23 snippet
std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp; // strong_count remains 1

if (auto locked = wp.lock()) {
    std::println("Object is alive: {}", *locked);
}
sp.reset(); // Destroys int: strong_count becomes 0
assert(wp.expired() == true);
assert(wp.lock() == nullptr);
Interview takeaway:

Use std::weak_ptr for back-pointers in trees/graphs, observer registries, and caches to prevent circular reference memory leaks.

05

Custom Deleters for Non-Memory OS Resources

Smart pointers are universal RAII containers for managing any operating system handle:

  1. Beyond Heap Memory: You can bind custom cleanup routines to manage C file handles (fclose), sockets (close), Windows handles (CloseHandle), or database connections.
  2. unique_ptr Custom Deleters (Type Parameter): The deleter type is baked into the template: std::unique_ptr<FILE, Decltype(&fclose)>. If the deleter is a stateless functor or lambda without captures, sizeof(unique_ptr) remains strictly 8 bytes.
  3. shared_ptr Custom Deleters (Type-Erased): The deleter is passed at construction time: std::shared_ptr<FILE>(fopen(...), fclose). The deleter is type-erased and stored inside the heap Control Block, keeping the shared_ptr type signature clean.
C++23 snippet
// Custom deleter for C FILE*
struct FileCloser {
    void operator()(FILE* fp) const noexcept {
        if (fp) std::fclose(fp);
    }
};
std::unique_ptr<FILE, FileCloser> file(std::fopen("log.txt", "w"));

// Custom deleter with shared_ptr (type-erased in Control Block):
std::shared_ptr<FILE> fileShared(std::fopen("data.bin", "rb"), &std::fclose);
Interview takeaway:

Use custom deleters with smart pointers to automate cleanup of OS handles, files, and sockets with full exception safety.

04

Phase 1 (15-min baseline): SimpleUniquePtr<T>: Move-Only Semantics, Deleted Copy & RAII Ownership

SimpleUniquePtr<T>: Move-Only Semantics, Deleted Copy & RAII OwnershipWrite this first in a 30-min interview

In a 30-minute interview on smart pointers and ownership, do not start by writing a multi-threaded shared_ptr with atomic control blocks, weak counters, and custom allocators. You will quickly run out of time. Instead, write this clean, ~40-line SimpleUniquePtr<T> baseline in the first 12 minutes. It demonstrates the non-negotiable core of C++ RAII: deleted copy operations, move construction/assignment via std::exchange, deterministic destruction, and pointer dereferencing with zero memory overhead.

Phase 1 — 15-Minute Whiteboard Baseline (Simple & Interview-Safe)
#include <cstddef>
#include <iostream>
#include <string>
#include <utility>

template <typename T>
class SimpleUniquePtr {
    T* ptr_{nullptr};

public:
    // 1. Constructors & Destructor
    explicit SimpleUniquePtr(T* p = nullptr) noexcept : ptr_(p) {}
    ~SimpleUniquePtr() noexcept { delete ptr_; }

    // 2. Deleted Copy Operations (Exclusive Ownership Invariant)
    SimpleUniquePtr(const SimpleUniquePtr&) = delete;
    SimpleUniquePtr& operator=(const SimpleUniquePtr&) = delete;

    // 3. Move Semantics: Transfer ownership and nullify source
    SimpleUniquePtr(SimpleUniquePtr&& other) noexcept
        : ptr_(std::exchange(other.ptr_, nullptr)) {}

    SimpleUniquePtr& operator=(SimpleUniquePtr&& other) noexcept {
        if (this != &other) {
            delete ptr_; // Free currently owned resource
            ptr_ = std::exchange(other.ptr_, nullptr);
        }
        return *this;
    }

    // 4. Observers & Modifiers
    T& operator*() const noexcept { return *ptr_; }
    T* operator->() const noexcept { return ptr_; }
    T* get() const noexcept { return ptr_; }
    explicit operator bool() const noexcept { return ptr_ != nullptr; }

    T* release() noexcept { return std::exchange(ptr_, nullptr); }
    void reset(T* p = nullptr) noexcept {
        if (ptr_ != p) {
            delete ptr_;
            ptr_ = p;
        }
    }
};

int main() {
    std::cout << "--- SimpleUniquePtr Construction & Dereference ---\n";
    SimpleUniquePtr<std::string> p1(new std::string("C++23 UniquePtr"));
    std::cout << "p1 value: " << *p1 << ", length: " << p1->length() << '\n';
    std::cout << "p1 holds resource: " << std::boolalpha << static_cast<bool>(p1) << '\n';

    std::cout << "\n--- Move Construction ---\n";
    SimpleUniquePtr<std::string> p2(std::move(p1));
    std::cout << "p1 is now: " << (p1 ? *p1 : "null") << '\n';
    std::cout << "p2 holds:  " << *p2 << '\n';

    std::cout << "\n--- Move Assignment ---\n";
    SimpleUniquePtr<std::string> p3;
    p3 = std::move(p2);
    std::cout << "p2 is now: " << (p2 ? *p2 : "null") << '\n';
    std::cout << "p3 holds:  " << *p3 << '\n';

    std::cout << "\n--- Reset & Release ---\n";
    p3.reset(new std::string("Replaced string"));
    std::cout << "p3 after reset: " << *p3 << '\n';

    std::string* raw = p3.release();
    std::cout << "p3 after release: " << (p3 ? *p3 : "null") << '\n';
    std::cout << "Released raw pointer: " << *raw << '\n';
    delete raw; // Manual deletion of released resource

    return 0;
}
Program output (stdout)
--- SimpleUniquePtr Construction & Dereference ---
p1 value: C++23 UniquePtr, length: 15
p1 holds resource: true

--- Move Construction ---
p1 is now: null
p2 holds:  C++23 UniquePtr

--- Move Assignment ---
p2 is now: null
p3 holds:  C++23 UniquePtr

--- Reset & Release ---
p3 after reset: Replaced string
p3 after release: null
Released raw pointer: Replaced string
Starter traps & limitations interviewer will probe
  • Forgetting to Delete Copy Constructor and Copy Assignment: If copy operations are not deleted (= delete), the compiler generates default memberwise copies. Two smart pointers will own the same raw pointer, causing an immediate double-free crash when both destruct.

  • Forgetting to Null Out Moved-From Pointer: In move constructor or move assignment, if other.ptr_ is not reset to nullptr (via std::exchange), both the destination and source still hold the address, triggering a double free.

  • Array Deletion Cookie Mismatch: delete ptr_ invokes scalar delete. If SimpleUniquePtr is instantiated as SimpleUniquePtr<int[]>, it must be specialized with delete[] ptr_ or array cookies are corrupted.

  • Lack of Custom Deleter Support & EBO: Standard std::unique_ptr<T, Deleter> uses the Empty Base Optimization (EBO) to support custom deleters (for FILE*, sockets, windows handles) at 0 bytes overhead, whereas this baseline only supports scalar heap memory.

The Senior Pivot Script (What to say at minute 15)

"This baseline implements the fundamental exclusive-ownership invariant of std::unique_ptr: move-only semantics, deleted copy operations, and deterministic RAII destruction with strictly 0 bytes of memory overhead. However, real-world distributed and multi-threaded systems often require shared ownership across threads, non-owning observer references, and custom cleanup handles for operating system descriptors. If you'd like, I can now pivot to Phase 2 to show how std::shared_ptr and std::weak_ptr manage their atomic control block lifecycles and break cyclic references."

05

Phase 2 (Production C++23): Smart Pointer Lifecycle: unique_ptr Move, shared_ptr Control Block & weak_ptr Upgrading

Smart Pointer Lifecycle: unique_ptr Move, shared_ptr Control Block & weak_ptr UpgradingCompiled & tested C++23

Implement a C++ program demonstrating: (1) std::unique_ptr ownership transfer via std::move, reset(), and a zero-overhead stateless custom deleter for a simulated socket handle, (2) std::shared_ptr reference count tracking (use_count()) through nested scope lifetimes, and (3) std::weak_ptr observing an object, upgrading safely via lock(), and detecting object expiration after reset().

Implementation walkthrough
  1. 1.

    Define SocketHandle simulating an OS socket descriptor and a stateless functor SocketDeleter.

  2. 2.

    Verify sizeof(unique_ptr<int>) == 8 and sizeof(unique_ptr<SocketHandle, SocketDeleter>) == 8, proving the zero-overhead invariant.

  3. 3.

    Demonstrate std::move transferring ownership from u1 to u2, confirming u1 is reset to nullptr and u2.reset() triggers cleanup.

  4. 4.

    Track sp1.use_count() as sp2 and sp3 are created and destroyed across nested scopes.

  5. 5.

    Bind a std::weak_ptr<int> to sp1, verify use_count() is unchanged, successfully call wp.lock(), then reset sp1 and verify wp.expired() == true and wp.lock() == nullptr.

impl.cpp — Live implementation (C++23)
#include <cstddef>
#include <memory>
#include <print>
#include <string>

// ============================================================================
// 1. UNIQUE_PTR & EXCLUSIVE RAII OWNERSHIP
// - sizeof(std::unique_ptr<T>) == 8 bytes (identical to raw pointer).
// - Zero memory overhead, zero runtime CPU cost compared to manual delete.
// - Stateless deleter: empty base class optimization (EBO) keeps size at 8 bytes.
// ============================================================================

// Custom resource simulating an OS socket descriptor
struct SocketHandle {
    int fd;
    explicit SocketHandle(int descriptor) : fd(descriptor) {
        std::println("  [SocketHandle] Opened fd={}", fd);
    }
    ~SocketHandle() {
        std::println("  [SocketHandle] Closed fd={}", fd);
    }
};

// Stateless custom deleter for unique_ptr (zero space overhead!)
struct SocketDeleter {
    void operator()(SocketHandle* sock) const noexcept {
        std::println("  [CustomDeleter] Executing custom socket cleanup for fd={}", sock ? sock->fd : -1);
        delete sock;
    }
};

int main() {
    std::println("--- 1. std::unique_ptr & Ownership Transfer ---");
    std::println("sizeof(unique_ptr<int>): {} bytes (raw pointer size)", sizeof(std::unique_ptr<int>));
    std::println("sizeof(unique_ptr with stateless deleter): {} bytes",
                 sizeof(std::unique_ptr<SocketHandle, SocketDeleter>));

    {
        // Step 1: std::make_unique allocates both object and pointer in single statement
        auto u1 = std::make_unique<std::string>("Exclusive Resource");
        std::println("u1 owns: \"{}\", valid = {}", *u1, u1 != nullptr);

        // Step 2: Ownership transfer via std::move
        std::unique_ptr<std::string> u2 = std::move(u1);
        std::println("After std::move: u1 is {}, u2 owns \"{}\"",
                     u1 ? "non-null" : "nullptr", *u2);

        // Step 3: Reset releases old object and takes ownership of new
        u2.reset(new std::string("Replaced Resource"));
        std::println("After reset: u2 owns \"{}\"", *u2);
    }

    std::println("\n--- Custom Deleter with unique_ptr ---");
    {
        std::unique_ptr<SocketHandle, SocketDeleter> sockPtr(new SocketHandle(42));
    } // SocketDeleter called automatically on scope exit

    // ========================================================================
    // 2. SHARED_PTR & CONTROL BLOCK TRACKING
    // - sizeof(std::shared_ptr<T>) == 16 bytes (pointer to object + pointer to control block).
    // - Atomic strong ref counting across nested scopes.
    // ========================================================================
    std::println("\n--- 2. std::shared_ptr & Control Block Tracking ---");
    std::println("sizeof(shared_ptr<int>): {} bytes (object ptr + control block ptr)",
                 sizeof(std::shared_ptr<int>));

    std::shared_ptr<int> sp1 = std::make_shared<int>(100);
    std::println("sp1 created: value = {}, use_count = {}", *sp1, sp1.use_count());
    {
        std::shared_ptr<int> sp2 = sp1;
        std::println("sp2 created (copy of sp1): sp1.use_count = {}, sp2.use_count = {}",
                     sp1.use_count(), sp2.use_count());
        {
            std::shared_ptr<int> sp3 = sp2;
            std::println("sp3 created (copy of sp2): use_count = {}", sp1.use_count());
        }
        std::println("sp3 destroyed: use_count = {}", sp1.use_count());
    }
    std::println("sp2 destroyed: use_count = {}", sp1.use_count());

    // ========================================================================
    // 3. WEAK_PTR: SAFE OBSERVER & UPGRADING
    // - sizeof(std::weak_ptr<T>) == 16 bytes.
    // - Observes object without incrementing use_count; upgraded via lock().
    // ========================================================================
    std::println("\n--- 3. std::weak_ptr Safe Observer & Upgrading ---");
    std::println("sizeof(weak_ptr<int>): {} bytes (object ptr + control block ptr)",
                 sizeof(std::weak_ptr<int>));

    std::weak_ptr<int> wp = sp1;
    std::println("weak_ptr bound to sp1: sp1.use_count = {}, wp.expired() = {}",
                 sp1.use_count(), wp.expired());

    if (auto locked = wp.lock()) {
        std::println("wp.lock() succeeded: value = {}, temp use_count = {}",
                     *locked, locked.use_count());
    }

    sp1.reset();
    std::println("sp1.reset() called: wp.expired() = {}", wp.expired());

    if (auto locked = wp.lock()) {
        std::println("wp.lock() succeeded unexpectedly");
    } else {
        std::println("wp.lock() returned nullptr because managed object is dead");
    }

    return 0;
}
Program output (stdout)
--- 1. std::unique_ptr & Ownership Transfer ---
sizeof(unique_ptr<int>): 8 bytes (raw pointer size)
sizeof(unique_ptr with stateless deleter): 8 bytes
u1 owns: "Exclusive Resource", valid = true
After std::move: u1 is nullptr, u2 owns "Exclusive Resource"
After reset: u2 owns "Replaced Resource"

--- Custom Deleter with unique_ptr ---
  [SocketHandle] Opened fd=42
  [CustomDeleter] Executing custom socket cleanup for fd=42
  [SocketHandle] Closed fd=42

--- 2. std::shared_ptr & Control Block Tracking ---
sizeof(shared_ptr<int>): 16 bytes (object ptr + control block ptr)
sp1 created: value = 100, use_count = 1
sp2 created (copy of sp1): sp1.use_count = 2, sp2.use_count = 2
sp3 created (copy of sp2): use_count = 3
sp3 destroyed: use_count = 2
sp2 destroyed: use_count = 1

--- 3. std::weak_ptr Safe Observer & Upgrading ---
sizeof(weak_ptr<int>): 16 bytes (object ptr + control block ptr)
weak_ptr bound to sp1: sp1.use_count = 1, wp.expired() = false
wp.lock() succeeded: value = 100, temp use_count = 2
sp1.reset() called: wp.expired() = true
wp.lock() returned nullptr because managed object is dead
Complexity & implementation notes
  • std::unique_ptr: O(1) move, 0 bytes overhead over raw pointer. No reference counting.
  • std::shared_ptr: O(1) copy/dtor, 16 bytes stack footprint. Reference counting uses atomic hardware instructions (lock xadd).
  • std::weak_ptr: O(1) lock/expired. Atomic compare-and-swap loop verifies strong count $> 0$.
06

Interviewer cross-questioning & viva playbook

Q1Why std::unique_ptr Cannot Be Copied & Ownership Transfer With/Without std::move
Live Viva Grill
Interviewer asks / writes on screen

"Why does std::unique_ptr explicitly delete its copy operations? How do you transfer ownership of a unique_ptr using std::move, and what are two valid ways to transfer ownership to a function WITHOUT calling std::move?"

Where candidates get trapped

Assuming std::move is the only way to move a unique_ptr, forgetting that unnamed temporaries (prvalues) bind directly to rvalue references, or not knowing about .release().

Senior mechanical explanation
  1. Why Copying is Forbidden: std::unique_ptr models exclusive ownership. If copying were allowed, two distinct unique_ptr instances would point to the exact same heap address. When the first instance leaves scope, it executes delete ptr;. When the second instance leaves scope, it executes delete ptr; again on already-freed memory—causing a catastrophic double free crash. To make this bug impossible at compile time, unique_ptr(const unique_ptr&) = delete;.
  2. Transferring Ownership with std::move: Because copy is deleted, passing a named lvalue u1 to sink(std::unique_ptr<T>) fails compilation. std::move(u1) casts u1 to an rvalue reference unique_ptr<T>&&, invoking the move constructor which transfers the 8-byte pointer to the parameter and sets u1 to nullptr.
  3. Transferring Ownership WITHOUT Calling std::move:
    • Way 1: Return Prvalue from Factory Function: sink(std::make_unique<Widget>(10)); or sink(createWidget());. Because the returned object is an unnamed temporary (prvalue), the language automatically binds it to the move constructor with zero copies and without writing std::move (or elides construction entirely via C++17 guaranteed copy elision).
    • Way 2: Manual .release() Handoff: std::unique_ptr<Widget> u2(u1.release());. Calling u1.release() sets u1's internal pointer to nullptr and returns the raw Widget*, which is immediately handed to u2's constructor. (Alternatively, explicit cast: static_cast<std::unique_ptr<Widget>&&>(u1)).
Mechanical proof (C++)
#include <memory>

void processWidget(std::unique_ptr<int> ptr) {
    // Takes sole ownership
}

std::unique_ptr<int> makeResource() {
    return std::make_unique<int>(100); // Prvalue return
}

int main() {
    auto u1 = std::make_unique<int>(42);
    // processWidget(u1); // COMPILE ERROR: Call to deleted copy constructor!

    // Method 1: Canonical std::move
    processWidget(std::move(u1)); // u1 is now nullptr

    // Method 2: Prvalue factory without std::move (guaranteed copy elision)
    processWidget(makeResource()); 

    // Method 3: Manual .release() transfer
    auto u2 = std::make_unique<int>(200);
    std::unique_ptr<int> u3(u2.release()); // u2 is nullptr, u3 owns 200
}
Q2make_shared 1-Allocation vs shared_ptr(new T) 2-Allocations & weak_ptr Retention
Live Viva Grill
Interviewer asks / writes on screen

"Compare std::make_shared<T>() with std::shared_ptr<T>(new T()). What are the memory allocation and cache differences? What is the hidden weak_ptr memory retention trade-off that surprises developers in production?"

Where candidates get trapped

Assuming std::make_shared is always superior in 100% of cases, not knowing that make_shared delays freeing the heap memory of T if long-lived weak_ptr instances remain.

Q3shared_ptr<T> Thread Safety: Control Block vs Pointed-To Object
Live Viva Grill
Interviewer asks / writes on screen

"Is std::shared_ptr thread-safe? What parts are atomic, what operations cause data races, and what happens if two threads concurrently write to the same object or mutate the same shared_ptr handle?"

Where candidates get trapped

Claiming that std::shared_ptr makes the underlying object thread-safe, or assuming multiple threads can safely read and overwrite the same shared_ptr handle variable without synchronization.

Q4Cyclic Reference Leaks & Why std::weak_ptr Breaks the Cycle
Live Viva Grill
Interviewer asks / writes on screen

"Demonstrate how a circular reference between two objects causes a permanent memory leak with std::shared_ptr. Explain how std::weak_ptr breaks the cycle and how wp.lock() guarantees safe, thread-safe access to expiring objects."

Where candidates get trapped

Not knowing why circular references prevent use_count() from reaching 0, or trying to dereference a weak_ptr directly (*wp) without calling lock().

07

Scenario: Breaking Circular Reference Memory Leaks in Doubly Linked Lists & Graph Nodes

The interviewer's twist

A caching service uses a doubly linked list where each node holds std::shared_ptr<Node> next and std::shared_ptr<Node> prev. During high-churn eviction, nodes are disconnected from the root list, yet memory usage keeps growing indefinitely until the server crashes with OOM. Valgrind reports that none of the node destructors are ever executed. Why did this happen, how do you reproduce the leak, and how do you fix it with std::weak_ptr so that 100% of nodes are destroyed cleanly?

How to handle this scenario

When Node A's next holds a shared_ptr to Node B, and Node B's prev holds a shared_ptr to Node A, both nodes maintain an internal strong_count of 2. When the outer pointers leave scope, both counts drop to 1, creating a cyclic ownership dead-lock. Neither node can ever reach strong_count == 0, leaking both nodes. We fix this by converting the back-pointer prev to std::weak_ptr<FixedNode>. Forward traversal maintains strong ownership, while backward traversal uses weak observation. When the root leaves scope, Node A's strong count drops to 0, destroying A, which drops B's strong count to 0, cleanly destroying B.

scenario.cpp — Scenario solution (C++23)
#include <memory>
#include <print>
#include <string>
#include <utility>

// ============================================================================
// SCENARIO: PREVENTING MEMORY LEAKS FROM CYCLIC SHARED_PTR REFERENCES
// Interview Twist: In a doubly linked list, node A points to B via shared_ptr,
// and node B points to A via shared_ptr. Both keep each other's use_count at 1,
// causing a permanent memory leak on scope exit.
// Fix: Use std::weak_ptr for the back-reference (prev).
// ============================================================================

// Global counters for destructor verification
inline int g_leakyDtorCalls = 0;
inline int g_fixedDtorCalls = 0;

// 1. LeakyNode: Uses std::shared_ptr for both next and prev (causing cyclic leak)
struct LeakyNode {
    std::string name;
    std::shared_ptr<LeakyNode> next;
    std::shared_ptr<LeakyNode> prev;

    explicit LeakyNode(std::string n) : name(std::move(n)) {
        std::println("  [LeakyNode Ctor] {}", name);
    }

    ~LeakyNode() {
        ++g_leakyDtorCalls;
        std::println("  [LeakyNode Dtor] {}", name);
    }
};

// 2. FixedNode: Uses std::weak_ptr for prev (breaking cyclic reference)
struct FixedNode {
    std::string name;
    std::shared_ptr<FixedNode> next;
    std::weak_ptr<FixedNode> prev;

    explicit FixedNode(std::string n) : name(std::move(n)) {
        std::println("  [FixedNode Ctor] {}", name);
    }

    ~FixedNode() {
        ++g_fixedDtorCalls;
        std::println("  [FixedNode Dtor] {}", name);
    }
};

int main() {
    std::println("--- Demonstrating Cyclic Reference Memory Leak ---");
    {
        auto nodeA = std::make_shared<LeakyNode>("NodeA");
        auto nodeB = std::make_shared<LeakyNode>("NodeB");

        nodeA->next = nodeB; // NodeA strongly owns NodeB
        nodeB->prev = nodeA; // NodeB strongly owns NodeA (CYCLIC DEADLOCK!)

        std::println("Inside scope: nodeA.use_count = {}, nodeB.use_count = {}",
                     nodeA.use_count(), nodeB.use_count());
    } // nodeA and nodeB go out of scope, but use_count drops to 1, not 0! Destructors NEVER run!

    std::println("Exited scope: g_leakyDtorCalls = {} (MEMORY LEAK: 0 destructors called!)",
                 g_leakyDtorCalls);

    std::println("\n--- Demonstrating Cycle Fix using std::weak_ptr ---");
    {
        auto nodeA = std::make_shared<FixedNode>("NodeA");
        auto nodeB = std::make_shared<FixedNode>("NodeB");

        nodeA->next = nodeB; // Forward strong ownership
        nodeB->prev = nodeA; // Backward weak observation (does NOT increment strong count!)

        std::println("Inside scope: nodeA.use_count = {}, nodeB.use_count = {}",
                     nodeA.use_count(), nodeB.use_count());

        if (auto parent = nodeB->prev.lock()) {
            std::println("nodeB safely navigated back to parent: {}", parent->name);
        }
    } // NodeB stack ref destroyed (use_count 2->1), NodeA stack ref destroyed (use_count 1->0) -> NodeA destructs -> nodeA->next destroyed -> NodeB destructs!

    std::println("Exited scope: g_fixedDtorCalls = {} (SUCCESS: 100% nodes destroyed cleanly!)",
                 g_fixedDtorCalls);

    return 0;
}
Program output (stdout)
--- Demonstrating Cyclic Reference Memory Leak ---
  [LeakyNode Ctor] NodeA
  [LeakyNode Ctor] NodeB
Inside scope: nodeA.use_count = 2, nodeB.use_count = 2
Exited scope: g_leakyDtorCalls = 0 (MEMORY LEAK: 0 destructors called!)

--- Demonstrating Cycle Fix using std::weak_ptr ---
  [FixedNode Ctor] NodeA
  [FixedNode Ctor] NodeB
Inside scope: nodeA.use_count = 1, nodeB.use_count = 2
nodeB safely navigated back to parent: NodeA
  [FixedNode Dtor] NodeA
  [FixedNode Dtor] NodeB
Exited scope: g_fixedDtorCalls = 2 (SUCCESS: 100% nodes destroyed cleanly!)
08

Common C++ mistakes & UB traps

Creating Multiple Independent shared_ptr Instances from the Same Raw Pointer
Buggy / UB code
int* raw = new int(10);
std::shared_ptr<int> sp1(raw);
std::shared_ptr<int> sp2(raw); // CRASH: Creates a second independent Control Block!
Safe / idiomatic C++
std::shared_ptr<int> sp1 = std::make_shared<int>(10);
std::shared_ptr<int> sp2 = sp1; // Correct: shares the same Control Block

Constructing two shared_ptr instances from the same raw pointer causes each to allocate its own separate Control Block with strong_count = 1. When sp1 goes out of scope, it deletes raw. When sp2 goes out of scope, it calls delete raw a second time, triggering a fatal double-free crash.

Passing shared_ptr by Value When Read-Only Access Suffices
Buggy / UB code
void inspect(std::shared_ptr<LargeData> data) { // Atomic increment & decrement on every call!
    std::println("{}", data->id());
}
Safe / idiomatic C++
void inspect(const LargeData& data) { // Zero overhead
    std::println("{}", data.id());
}

Passing std::shared_ptr<T> by value creates a temporary copy on every call, executing two atomic operations (atomic_fetch_add on entry, atomic_fetch_sub on exit) that flush CPU store buffers. Unless the function participates in ownership (e.g. stores the pointer in a queue), pass by const T& or const std::shared_ptr<T>&.

Calling shared_from_this() in a Constructor
Buggy / UB code
struct Listener : std::enable_shared_from_this<Listener> {
    Listener() {
        auto self = shared_from_this(); // CRASH: std::bad_weak_ptr exception!
    }
};
Safe / idiomatic C++
struct Listener : std::enable_shared_from_this<Listener> {
    void init() {
        auto self = shared_from_this(); // Safe: Control Block has already been constructed
    }
};

shared_from_this() requires the object's weak_ptr inside enable_shared_from_this to already be initialized by an active std::shared_ptr. During the constructor execution, the shared_ptr owner has not finished instantiating, so shared_from_this() throws std::bad_weak_ptr.

09

"What does this print / Why does this crash?"

Puzzle 1: The make_shared Weak Pointer Memory Trap
Predict the behavior
struct BigBuffer { char data[1024 * 1024 * 10]; }; // 10 MB
std::weak_ptr<BigBuffer> globalWp;
{
    auto sp = std::make_shared<BigBuffer>();
    globalWp = sp;
} // sp destroyed: ~BigBuffer() runs!
std::println("expired: {}", globalWp.expired());
Puzzle 2: Custom Deleter Size Puzzle: unique_ptr vs shared_ptr
Predict the behavior
auto lambdaDeleter = [](int* p) { delete p; };
std::unique_ptr<int, decltype(lambdaDeleter)> u(new int(1), lambdaDeleter);
std::shared_ptr<int> s(new int(2), lambdaDeleter);
std::println("sizeof(u): {}, sizeof(s): {}", sizeof(u), sizeof(s));
10

Rapid-fire interview Q&A

What is the memory size of std::unique_ptr and std::shared_ptr on a 64-bit architecture?

Why is std::make_shared preferred over std::shared_ptr<T>(new T)? Are there any drawbacks?

Is std::shared_ptr thread-safe in C++?

How does std::weak_ptr prevent cyclic reference memory leaks?

What happens when you call wp.lock() on an expired std::weak_ptr?

Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering