Smart Pointers (unique_ptr, shared_ptr, weak_ptr)
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).
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.
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.
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).
Interviewer's lens
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_ptris a zero-cost wrapper around an 8-byte pointer, whileshared_ptrmanages a complex heap Control Block using multi-core atomic operations. - What Evaluators Test:
- Zero-Overhead Proof: Why
sizeof(std::unique_ptr<T>) == 8adds zero runtime cost via the Empty Base Optimization (EBO). - Control Block Deep Dive: How
std::make_sharedperforms 1 single contiguous heap allocation versus 2 separate allocations forshared_ptr<T>(new T), and the weak-reference memory retention trade-off. - Cycle Breaking & Thread Safety: Why cyclic
shared_ptrgraphs leak memory permanently, howweak_ptr::lock()atomizes access, and which operations are thread-safe.
- Zero-Overhead Proof: Why
Under the hood & memory layout
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:
std::unique_ptr(8 Bytes): Holds a single raw pointerT*. With a stateless deleter, the Empty Base Optimization (EBO) reduces the deleter to 0 bytes—yielding zero memory overhead and zero runtime penalty.std::shared_ptr(16 Bytes): Holds two 8-byte pointers: one to the objectT*, and one to the heap Control Block.- The Control Block Lifecycle: Houses atomic strong and weak counters, custom deleters, and allocators:
- Copying a
shared_ptrexecutes 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.
- Copying a
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.
Stores two 8-byte pointers: T* ptr (points to target object) and ControlBlock* cb (points to the shared reference counting 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.
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).
Core concepts & syntax traps
Exclusive Ownership: std::unique_ptr & Zero-Overhead Invariant
Exclusive Ownership: std::unique_ptr & Zero-Overhead Invariant
A std::unique_ptr<T> represents single, unquestioned ownership over a resource:
- Move-Only Contract: Copy constructor and copy assignment are explicitly deleted (
= delete). Ownership transfers strictly throughstd::move(), which hands over the raw pointer and sets the source variable tonullptr. - Zero Memory Overhead: On 64-bit platforms,
sizeof(std::unique_ptr<T>) == 8bytes—identical to a raw pointer. - 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. - Deterministic Destruction: The instant the owning
unique_ptrfalls out of scope, it invokes the deleter and reclaims the resource with zero runtime indirection.
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!Default to std::unique_ptr for all dynamic allocations; it provides full RAII safety with zero memory or runtime overhead.
Shared Ownership: std::shared_ptr & The Control Block
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>:
- 16-Byte Footprint: Every
shared_ptrstores two 8-byte pointers:- A pointer to the managed object
T*. - A pointer to the shared heap Control Block.
- A pointer to the managed object
- Atomic Reference Counting:
- Copying a
shared_ptrinvokesstd::atomic::fetch_addonstrong_count. This CPU bus lock prevents multi-threaded race conditions but adds latency. - Destroying a
shared_ptrdecrementsstrong_count. When it hits 0, the destructor~T()is executed.
- Copying a
- 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.
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()); // 1Pass shared_ptr by const reference (const std::shared_ptr<T>&) to avoid costly atomic reference counter increments.
std::make_shared vs std::shared_ptr<T>(new T): The 1-Heap vs 2-Heap Allocation Trade-off
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:
- The 2-Allocation Approach (
shared_ptr<T>(new T)):- Allocation 1:
new Tallocates memory on the heap for the object. - Allocation 2: The
shared_ptrconstructor callsmallocagain to create the separate Control Block. - Downsides: Two allocator roundtrips, fragmented memory addresses, and poor cache locality.
- Allocation 1:
- The 1-Allocation Approach (
std::make_shared<T>):- Performs a single contiguous heap allocation housing both the Control Block and the object
Tback-to-back. - Benefits: Halves allocator overhead and ensures the object and control block share the same cache line.
- Performs a single contiguous heap allocation housing both the Control Block and the object
- 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_countANDweak_counthit 0. If aweak_ptroutlives the object, memory remains allocated even after~T()runs.
// 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>();Always use std::make_shared unless you require a custom deleter or have long-lived weak_ptr instances holding large objects in memory.
Non-Owning Observers: std::weak_ptr & Breaking Cyclic Reference Leaks
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:
- The Cyclic Deadlock: If Object A holds a
shared_ptrto Object B, and B holds ashared_ptrto A, their strong reference counts can never drop below 1. When all outside references go away, neither destructor runs, causing a permanent memory leak. - The Observer Role (
std::weak_ptr<T>): Aweak_ptrobserves an object by pointing to its Control Block, incrementing onlyweak_count. It exerts no ownership and does not prevent~T()from running. - Safe Access via
lock(): Because a weak pointer doesn't keep the object alive, direct dereferencing (*wp) is illegal. You must callwp.lock(), which atomically checksstrong_countand returns a validshared_ptr<T>—ornullptrif the object has already been destroyed.
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);Use std::weak_ptr for back-pointers in trees/graphs, observer registries, and caches to prevent circular reference memory leaks.
Custom Deleters for Non-Memory OS Resources
Custom Deleters for Non-Memory OS Resources
Smart pointers are universal RAII containers for managing any operating system handle:
- Beyond Heap Memory: You can bind custom cleanup routines to manage C file handles (
fclose), sockets (close), Windows handles (CloseHandle), or database connections. unique_ptrCustom 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.shared_ptrCustom 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 theshared_ptrtype signature clean.
// 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);Use custom deleters with smart pointers to automate cleanup of OS handles, files, and sockets with full exception safety.
Phase 1 (15-min baseline): SimpleUniquePtr<T>: Move-Only Semantics, Deleted Copy & RAII Ownership
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.
#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;
}--- 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
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 tonullptr(viastd::exchange), both the destination and source still hold the address, triggering a double free.Array Deletion Cookie Mismatch:
delete ptr_invokes scalardelete. IfSimpleUniquePtris instantiated asSimpleUniquePtr<int[]>, it must be specialized withdelete[] 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 (forFILE*, sockets, windows handles) at 0 bytes overhead, whereas this baseline only supports scalar heap memory.
"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."
Phase 2 (Production C++23): Smart Pointer Lifecycle: unique_ptr Move, shared_ptr Control Block & weak_ptr Upgrading
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().
- 1.
Define
SocketHandlesimulating an OS socket descriptor and a stateless functorSocketDeleter. - 2.
Verify
sizeof(unique_ptr<int>) == 8andsizeof(unique_ptr<SocketHandle, SocketDeleter>) == 8, proving the zero-overhead invariant. - 3.
Demonstrate
std::movetransferring ownership fromu1tou2, confirmingu1is reset tonullptrandu2.reset()triggers cleanup. - 4.
Track
sp1.use_count()assp2andsp3are created and destroyed across nested scopes. - 5.
Bind a
std::weak_ptr<int>tosp1, verifyuse_count()is unchanged, successfully callwp.lock(), then resetsp1and verifywp.expired() == trueandwp.lock() == nullptr.
#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;
}--- 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
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$.
Interviewer cross-questioning & viva playbook
"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?"
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().
- Why Copying is Forbidden:
std::unique_ptrmodels exclusive ownership. If copying were allowed, two distinctunique_ptrinstances would point to the exact same heap address. When the first instance leaves scope, it executesdelete ptr;. When the second instance leaves scope, it executesdelete 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;. - Transferring Ownership with
std::move: Because copy is deleted, passing a named lvalueu1tosink(std::unique_ptr<T>)fails compilation.std::move(u1)castsu1to an rvalue referenceunique_ptr<T>&&, invoking the move constructor which transfers the 8-byte pointer to the parameter and setsu1tonullptr. - Transferring Ownership WITHOUT Calling
std::move:- Way 1: Return Prvalue from Factory Function:
sink(std::make_unique<Widget>(10));orsink(createWidget());. Because the returned object is an unnamed temporary (prvalue), the language automatically binds it to the move constructor with zero copies and without writingstd::move(or elides construction entirely via C++17 guaranteed copy elision). - Way 2: Manual
.release()Handoff:std::unique_ptr<Widget> u2(u1.release());. Callingu1.release()setsu1's internal pointer tonullptrand returns the rawWidget*, which is immediately handed tou2's constructor. (Alternatively, explicit cast:static_cast<std::unique_ptr<Widget>&&>(u1)).
- Way 1: Return Prvalue from Factory Function:
#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
}"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?"
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.
"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?"
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.
"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."
Not knowing why circular references prevent use_count() from reaching 0, or trying to dereference a weak_ptr directly (*wp) without calling lock().
Scenario: Breaking Circular Reference Memory Leaks in Doubly Linked Lists & Graph Nodes
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?
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.
#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;
}--- 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!)
Common C++ mistakes & UB traps
int* raw = new int(10); std::shared_ptr<int> sp1(raw); std::shared_ptr<int> sp2(raw); // CRASH: Creates a second independent Control Block!
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.
void inspect(std::shared_ptr<LargeData> data) { // Atomic increment & decrement on every call!
std::println("{}", data->id());
}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>&.
struct Listener : std::enable_shared_from_this<Listener> {
Listener() {
auto self = shared_from_this(); // CRASH: std::bad_weak_ptr exception!
}
};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.
"What does this print / Why does this crash?"
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());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));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?