Project 3: Smart Pointer Implementation

Build your own unique_ptr, shared_ptr, and weak_ptr with proper move semantics, thread-safe reference counting, and custom deleters.

Quick Reference

Attribute Value
Difficulty Advanced
Time Estimate 2-3 weeks
Language C++
Prerequisites Projects 1-2, constructors/destructors, move semantics basics
Key Topics RAII, move semantics, atomic operations, control blocks, type erasure

1. Learning Objectives

After completing this project, you will:

  • Master RAII (Resource Acquisition Is Initialization) deeply
  • Understand move semantics at the implementation level
  • Learn thread-safe reference counting with atomic operations
  • Understand the control block pattern used in shared_ptr/weak_ptr
  • Implement type erasure for custom deleters
  • Know exactly how the standard library smart pointers work internally

2. Theoretical Foundation

2.1 Core Concepts

RAII: Resource Acquisition Is Initialization

RAII is the C++ idiom where resource lifetime is tied to object lifetime:

  • Resource acquired in constructor
  • Resource released in destructor
  • The compiler guarantees destructors run (for automatic storage)
// Without RAII: error-prone
void process() {
    int* data = new int[1000];
    // ... code that might throw or return early ...
    delete[] data;  // Might never run!
}

// With RAII: safe
void process() {
    std::unique_ptr<int[]> data(new int[1000]);
    // ... code can throw or return ...
    // Destructor runs automatically
}

Ownership Semantics

C++ has three ownership models:

Type Ownership Copy Move Use Case
unique_ptr Exclusive Forbidden Allowed Single owner, clear ownership
shared_ptr Shared Allowed (ref++) Allowed Multiple owners
weak_ptr Non-owning Allowed Allowed Observer, break cycles

Move Semantics

Move semantics allow transferring resources instead of copying:

std::unique_ptr<Widget> p1 = std::make_unique<Widget>();
std::unique_ptr<Widget> p2 = std::move(p1);  // Transfer ownership
// p1 is now null, p2 owns the Widget

This requires:

  • Move constructor: T(T&& other)
  • Move assignment: T& operator=(T&& other)
  • Deleted copy operations (for unique_ptr)

Reference Counting

shared_ptr uses reference counting:

  • Each shared_ptr to the same object increments a counter
  • Destructor decrements the counter
  • When counter reaches 0, the object is deleted
shared_ptr sp1(new Widget);  // count = 1
shared_ptr sp2 = sp1;        // count = 2
sp1.reset();                 // count = 1
sp2.reset();                 // count = 0, Widget deleted

The Control Block

shared_ptr and weak_ptr share a control block:

+------------------+     +------------------+
| shared_ptr sp1   |---->|  Control Block   |
+------------------+     |------------------|
                         | strong_count: 2  |
+------------------+     | weak_count: 2    |
| shared_ptr sp2   |---->| T* ptr          |
+------------------+     | Deleter         |
                         +------------------+
+------------------+            |
| weak_ptr wp      |------------|
+------------------+

Object deleted when strong_count == 0
Control block deleted when strong_count == 0 AND weak_count == 0

Thread Safety

Reference counts must be thread-safe:

  • Use std::atomic<size_t> for counts
  • Memory ordering: memory_order_relaxed for increment, memory_order_acq_rel for decrement

2.2 Why This Matters

Smart pointers are the way to manage memory in modern C++:

  • Every modern C++ codebase uses them
  • Core Guidelines: “Never use raw new and delete
  • Understanding their internals makes you a better C++ programmer
  • Job interviews frequently ask about smart pointer internals

2.3 Historical Context

Before C++11:

  • Manual new/delete everywhere
  • Smart pointers existed (auto_ptr, Boost) but were limited
  • Memory leaks and dangling pointers were common bugs

C++11 introduced:

  • unique_ptr (replacing broken auto_ptr)
  • shared_ptr and weak_ptr (from Boost)
  • Move semantics enabling non-copyable unique_ptr

C++14 added make_unique. C++17 added array support to shared_ptr.

2.4 Common Misconceptions

Misconception 1: “shared_ptr is always better than unique_ptr” False. unique_ptr has zero overhead—same as raw pointer. shared_ptr has reference counting overhead. Use unique_ptr by default.

Misconception 2: “shared_ptr is thread-safe” Partially true. The control block is thread-safe. But:

  • Modifying the pointed-to object is NOT thread-safe
  • Modifying the shared_ptr itself (e.g., reset) from multiple threads is NOT safe

Misconception 3: “weak_ptr prevents all cycles” Only if you use it correctly. You must identify which pointers should be weak to break cycles.

Misconception 4: “make_shared is just for convenience” False. make_shared does one allocation (object + control block together), while shared_ptr(new T) does two allocations.


3. Project Specification

3.1 What You Will Build

Custom implementations of:

  1. MyUniquePtr<T> - Exclusive ownership smart pointer
  2. MySharedPtr<T> - Shared ownership with reference counting
  3. MyWeakPtr<T> - Non-owning observer that can lock to shared_ptr
  4. Custom deleter support for both unique and shared pointers
  5. makeUnique<T> and makeShared<T> factory functions

3.2 Functional Requirements

ID Requirement
F1 MyUniquePtr: exclusive ownership, move-only
F2 MyUniquePtr: support get(), release(), reset(), operator*, operator->
F3 MyUniquePtr: support custom deleters
F4 MySharedPtr: shared ownership with reference counting
F5 MySharedPtr: support use_count(), unique(), reset(), operator*, operator->
F6 MySharedPtr: thread-safe reference counting
F7 MyWeakPtr: non-owning observer
F8 MyWeakPtr: support lock(), expired(), use_count()
F9 makeUnique and makeShared factory functions
F10 Support nullptr assignment and comparison

3.3 Non-Functional Requirements

ID Requirement
N1 MyUniquePtr: same size as raw pointer (no overhead)
N2 MySharedPtr: thread-safe for concurrent use_count changes
N3 No memory leaks (verified with Valgrind/ASan)
N4 Compile with -Wall -Wextra without warnings
N5 Works with any type T, including incomplete types for unique_ptr

3.4 Example Usage / Output

// unique_ptr - exclusive ownership
MyUniquePtr<Widget> up1 = makeUnique<Widget>(42);
// MyUniquePtr<Widget> up2 = up1;  // ERROR: deleted copy constructor
MyUniquePtr<Widget> up2 = std::move(up1);  // OK: transfer ownership
assert(up1.get() == nullptr);
assert(up2->getValue() == 42);

// unique_ptr with custom deleter
auto fileDeleter = [](FILE* f) { if (f) fclose(f); };
MyUniquePtr<FILE, decltype(fileDeleter)> file(fopen("test.txt", "r"), fileDeleter);

// shared_ptr - shared ownership
MySharedPtr<Widget> sp1 = makeShared<Widget>(100);
MySharedPtr<Widget> sp2 = sp1;  // OK: shared ownership
assert(sp1.use_count() == 2);
sp1.reset();
assert(sp2.use_count() == 1);
assert(sp2->getValue() == 100);

// weak_ptr - non-owning observer
MyWeakPtr<Widget> wp = sp2;
assert(!wp.expired());
if (auto locked = wp.lock()) {
    std::cout << locked->getValue() << "\n";  // 100
}
sp2.reset();
assert(wp.expired());
assert(wp.lock() == nullptr);

// Custom deleter with shared_ptr
auto arrayDeleter = [](int* p) { delete[] p; };
MySharedPtr<int> arr(new int[100], arrayDeleter);

3.5 Real World Outcome

After completing this project, you will:

  • Understand exactly how standard library smart pointers work
  • Know when to use each smart pointer type
  • Be able to debug smart pointer issues in production code
  • Ace any interview question about smart pointer internals

4. Solution Architecture

4.1 High-Level Design

+-------------------+
|   MyUniquePtr     |
|-------------------|     +------------------+
| T* ptr_           |---->|  Managed Object  |
| Deleter deleter_  |     +------------------+
+-------------------+


+-------------------+     +--------------------+     +------------------+
|   MySharedPtr     |---->|   Control Block    |---->|  Managed Object  |
|-------------------|     |--------------------|     +------------------+
| T* ptr_           |     | atomic strong_cnt  |
| ControlBlock* cb_ |     | atomic weak_cnt    |
+-------------------+     | T* ptr             |
                          | Deleter deleter    |
+-------------------+     +--------------------+
|   MyWeakPtr       |-------------|
|-------------------|
| T* ptr_           |
| ControlBlock* cb_ |
+-------------------+

4.2 Key Components

MyUniquePtr

template<typename T, typename Deleter = std::default_delete<T>>
class MyUniquePtr {
    T* ptr_ = nullptr;
    Deleter deleter_;

public:
    // Constructors
    MyUniquePtr() = default;
    explicit MyUniquePtr(T* p);
    MyUniquePtr(T* p, Deleter d);

    // Disable copy
    MyUniquePtr(const MyUniquePtr&) = delete;
    MyUniquePtr& operator=(const MyUniquePtr&) = delete;

    // Enable move
    MyUniquePtr(MyUniquePtr&& other) noexcept;
    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept;

    // Destructor
    ~MyUniquePtr();

    // Observers
    T* get() const noexcept;
    T& operator*() const;
    T* operator->() const;
    explicit operator bool() const noexcept;

    // Modifiers
    T* release() noexcept;
    void reset(T* p = nullptr);
    void swap(MyUniquePtr& other) noexcept;
};

ControlBlock (for shared/weak)

struct ControlBlockBase {
    std::atomic<size_t> strong_count{1};
    std::atomic<size_t> weak_count{1};  // +1 for strong refs

    virtual void destroy_object() = 0;
    virtual void destroy_self() = 0;
    virtual ~ControlBlockBase() = default;

    void add_strong();
    void release_strong();
    void add_weak();
    void release_weak();
    bool try_add_strong();  // For weak_ptr::lock()
};

template<typename T, typename Deleter>
struct ControlBlock : ControlBlockBase {
    T* ptr;
    Deleter deleter;

    void destroy_object() override { deleter(ptr); }
    void destroy_self() override { delete this; }
};

MySharedPtr

template<typename T>
class MySharedPtr {
    T* ptr_ = nullptr;
    ControlBlockBase* cb_ = nullptr;

public:
    // Constructors
    MySharedPtr() = default;
    explicit MySharedPtr(T* p);
    template<typename Deleter>
    MySharedPtr(T* p, Deleter d);

    // Copy
    MySharedPtr(const MySharedPtr& other);
    MySharedPtr& operator=(const MySharedPtr& other);

    // Move
    MySharedPtr(MySharedPtr&& other) noexcept;
    MySharedPtr& operator=(MySharedPtr&& other) noexcept;

    // From weak_ptr
    explicit MySharedPtr(const MyWeakPtr<T>& wp);

    // Destructor
    ~MySharedPtr();

    // Observers
    T* get() const noexcept;
    T& operator*() const;
    T* operator->() const;
    size_t use_count() const noexcept;
    bool unique() const noexcept;
    explicit operator bool() const noexcept;

    // Modifiers
    void reset();
    void reset(T* p);
    void swap(MySharedPtr& other) noexcept;
};

4.3 Data Structures

Structure Purpose
T* ptr_ Raw pointer to managed object
ControlBlockBase* Pointer to shared control block
atomic<size_t> Thread-safe reference counts
Deleter Type-erased or templated deleter

4.4 Algorithm Overview

Reference Counting with Atomics

void ControlBlockBase::add_strong() {
    strong_count.fetch_add(1, std::memory_order_relaxed);
}

void ControlBlockBase::release_strong() {
    if (strong_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
        destroy_object();
        release_weak();  // Release the implicit weak reference
    }
}

void ControlBlockBase::add_weak() {
    weak_count.fetch_add(1, std::memory_order_relaxed);
}

void ControlBlockBase::release_weak() {
    if (weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
        destroy_self();
    }
}

bool ControlBlockBase::try_add_strong() {
    size_t count = strong_count.load(std::memory_order_relaxed);
    while (count > 0) {
        if (strong_count.compare_exchange_weak(
                count, count + 1,
                std::memory_order_acq_rel,
                std::memory_order_relaxed)) {
            return true;
        }
    }
    return false;
}

Why memory_order_acq_rel for decrement?

  • Acquire: ensure we see all previous modifications to the object
  • Release: ensure our modifications are visible to whoever deletes

5. Implementation Guide

5.1 Development Environment Setup

mkdir smart_pointers && cd smart_pointers

# Create files
touch my_unique_ptr.hpp my_shared_ptr.hpp my_weak_ptr.hpp control_block.hpp
touch main.cpp test_smart_pointers.cpp

# Compile (need -pthread for atomics on some systems)
g++ -std=c++17 -Wall -Wextra -pthread -o test test_smart_pointers.cpp

# With sanitizers for testing
g++ -std=c++17 -Wall -Wextra -fsanitize=address,undefined -o test test_smart_pointers.cpp

5.2 Project Structure

smart_pointers/
├── include/
│   ├── control_block.hpp
│   ├── my_unique_ptr.hpp
│   ├── my_shared_ptr.hpp
│   └── my_weak_ptr.hpp
├── src/
│   └── main.cpp
└── tests/
    ├── test_unique_ptr.cpp
    ├── test_shared_ptr.cpp
    └── test_weak_ptr.cpp

5.3 The Core Question You’re Answering

“How do I automate memory management without garbage collection, giving me precise control over object lifetimes?”

This decomposes into:

  1. How do I tie resource cleanup to object destruction (RAII)?
  2. How do I prevent copying for exclusive ownership (unique_ptr)?
  3. How do I share ownership among multiple pointers (shared_ptr)?
  4. How do I observe without owning (weak_ptr)?
  5. How do I make reference counting thread-safe?

5.4 Concepts You Must Understand First

Question Book Reference
What is a destructor and when is it called? Effective C++ Item 5
What are copy constructor and copy assignment? Effective C++ Items 5, 11
What are rvalue references and std::move? Effective Modern C++ Items 23-30
What is std::atomic and memory ordering? C++ Concurrency in Action Ch. 5
What is type erasure? Effective Modern C++ Item 39

5.5 Questions to Guide Your Design

unique_ptr Design

  • Why must the copy constructor be deleted?
  • What should happen when you move from a unique_ptr?
  • How do you store a custom deleter without overhead when using default_delete?
  • What happens if T is an incomplete type (forward-declared)?

shared_ptr Design

  • Where is the control block allocated?
  • Why is there a separate weak_count from strong_count?
  • What order should operations happen in the copy constructor?
  • What happens when you construct shared_ptr from a raw pointer twice?

weak_ptr Design

  • Why doesn’t weak_ptr have operator* and operator->?
  • What does lock() return if the object is deleted?
  • How does try_add_strong() work atomically?

Thread Safety

  • Why is memory_order_relaxed okay for increment but not decrement?
  • What’s a race condition in reference counting?
  • How do you prevent ABA problems in try_add_strong?

5.6 Thinking Exercise

Trace through this code:

MySharedPtr<Widget> sp1 = makeShared<Widget>(42);  // (1)
{
    MySharedPtr<Widget> sp2 = sp1;                  // (2)
    MyWeakPtr<Widget> wp = sp2;                     // (3)
    sp1.reset();                                     // (4)
    auto sp3 = wp.lock();                           // (5)
}                                                    // (6)

At each numbered point:

  1. After (1): strong=1, weak=1, Widget exists
  2. After (2): strong=2, weak=1, Widget exists
  3. After (3): strong=2, weak=2, Widget exists
  4. After (4): strong=1, weak=2, Widget exists (sp2 still owns)
  5. After (5): strong=2, weak=2 (sp3 locked successfully)
  6. After (6): strong=0, weak=1, Widget destroyed, control block remains

Then when wp goes out of scope: weak=0, control block destroyed.

5.7 Hints in Layers

Hint 1: Starting Point (Conceptual) Start with unique_ptr—it’s simpler (no control block, no atomics). Then do shared_ptr. Finally weak_ptr.

Hint 2: Next Level (More Specific)

unique_ptr skeleton:

template<typename T, typename Deleter = std::default_delete<T>>
class MyUniquePtr {
    T* ptr_ = nullptr;
    [[no_unique_address]] Deleter deleter_;  // Empty base optimization

public:
    MyUniquePtr() noexcept = default;
    explicit MyUniquePtr(T* p) noexcept : ptr_(p) {}

    ~MyUniquePtr() { reset(); }

    // Delete copy
    MyUniquePtr(const MyUniquePtr&) = delete;
    MyUniquePtr& operator=(const MyUniquePtr&) = delete;

    // Move
    MyUniquePtr(MyUniquePtr&& other) noexcept
        : ptr_(other.ptr_), deleter_(std::move(other.deleter_)) {
        other.ptr_ = nullptr;
    }

    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
        if (this != &other) {
            reset();
            ptr_ = other.ptr_;
            deleter_ = std::move(other.deleter_);
            other.ptr_ = nullptr;
        }
        return *this;
    }

    void reset(T* p = nullptr) {
        T* old = ptr_;
        ptr_ = p;
        if (old) deleter_(old);
    }

    T* release() noexcept {
        T* p = ptr_;
        ptr_ = nullptr;
        return p;
    }

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

Hint 3: Technical Details (Control Block)

struct ControlBlockBase {
    std::atomic<size_t> strong_count{1};
    std::atomic<size_t> weak_count{1};

    virtual void destroy_object() = 0;
    virtual void destroy_self() = 0;
    virtual ~ControlBlockBase() = default;

    void add_strong() {
        strong_count.fetch_add(1, std::memory_order_relaxed);
    }

    void release_strong() {
        if (strong_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            destroy_object();
            release_weak();
        }
    }

    void add_weak() {
        weak_count.fetch_add(1, std::memory_order_relaxed);
    }

    void release_weak() {
        if (weak_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            destroy_self();
        }
    }

    bool try_add_strong() {
        size_t count = strong_count.load(std::memory_order_relaxed);
        while (count != 0) {
            if (strong_count.compare_exchange_weak(count, count + 1,
                    std::memory_order_acq_rel, std::memory_order_relaxed)) {
                return true;
            }
        }
        return false;
    }
};

Hint 4: Implementation Details (shared_ptr with deleter)

template<typename T, typename Deleter>
struct ControlBlockWithDeleter : ControlBlockBase {
    T* ptr;
    Deleter deleter;

    ControlBlockWithDeleter(T* p, Deleter d) : ptr(p), deleter(std::move(d)) {}

    void destroy_object() override {
        deleter(ptr);
        ptr = nullptr;
    }

    void destroy_self() override {
        delete this;
    }
};

template<typename T>
class MySharedPtr {
    T* ptr_ = nullptr;
    ControlBlockBase* cb_ = nullptr;

public:
    template<typename Deleter>
    MySharedPtr(T* p, Deleter d)
        : ptr_(p)
        , cb_(new ControlBlockWithDeleter<T, Deleter>(p, std::move(d)))
    {}

    // ... rest of implementation
};

5.8 The Interview Questions They’ll Ask

  1. “Explain the difference between unique_ptr and shared_ptr.”
    • unique_ptr: exclusive ownership, move-only, zero overhead
    • shared_ptr: shared ownership, copyable, reference counting overhead
  2. “How is shared_ptr reference counting thread-safe?”
    • Uses std::atomic for counts
    • Increment: memory_order_relaxed (no ordering needed)
    • Decrement: memory_order_acq_rel (must synchronize with delete)
  3. “What is weak_ptr and why would you use it?”
    • Non-owning observer of shared_ptr
    • Use to break circular references
    • Use for caches (can observe without keeping alive)
  4. “What happens if you create two shared_ptrs from the same raw pointer?”
    • Two independent control blocks
    • Double-free when both reach zero
    • This is why you use make_shared or pass by copy
  5. “Why is make_shared more efficient than shared_ptr(new T)?”
    • shared_ptr(new T): two allocations (object + control block)
    • make_shared: one allocation (combined object + control block)
    • Better cache locality, fewer allocation overheads

5.9 Books That Will Help

Topic Book Chapter
Move Semantics Effective Modern C++ Items 23-30
Smart Pointers Effective Modern C++ Items 18-22
Atomic Operations C++ Concurrency in Action Chapter 5
RAII Effective C++ Item 13
Special Member Functions Effective C++ Items 5, 6, 11, 14

5.10 Implementation Phases

Phase 1: Basic unique_ptr (Day 1-3)

  • Default constructor, pointer constructor
  • Move constructor and move assignment
  • Destructor
  • get(), operator*, operator->
  • reset(), release()
  • Test: verify single ownership, no leaks

Phase 2: unique_ptr with Custom Deleter (Day 4-5)

  • Add template parameter for Deleter
  • Store deleter, call it in destructor
  • Test: custom deleter for FILE*, arrays

Phase 3: Basic shared_ptr (Day 6-9)

  • ControlBlock with strong_count only (single-threaded first)
  • Constructor, copy, move
  • Destructor with reference counting
  • use_count(), reset()
  • Test: verify shared ownership

Phase 4: Thread-Safe Reference Counting (Day 10-11)

  • Change to std::atomic
  • Add proper memory ordering
  • Test: concurrent access from multiple threads

Phase 5: weak_ptr (Day 12-15)

  • Add weak_count to control block
  • Implement WeakPtr class
  • Implement lock() with try_add_strong()
  • Implement expired()
  • Test: verify object survives only with strong refs

Phase 6: makeUnique and makeShared (Day 16-17)

  • Variadic templates for forwarding arguments
  • makeShared with combined allocation (advanced)
  • Test: verify factory functions work

5.11 Key Implementation Decisions

Decision Options Recommendation
Deleter storage Always store vs EBO Use [[no_unique_address]] for EBO
Control block Virtual vs type-erased Virtual (simpler)
weak_count initial 0 vs 1 1 (for implicit strong reference)
nullptr handling Separate path vs unified Unified (cb_ == nullptr means empty)
make_shared allocation Separate vs combined Combined for efficiency (optional)

6. Testing Strategy

6.1 Unit Tests

// unique_ptr tests
void testUniquePtrMove() {
    MyUniquePtr<int> p1(new int(42));
    MyUniquePtr<int> p2 = std::move(p1);
    assert(p1.get() == nullptr);
    assert(*p2 == 42);
}

void testUniquePtrCustomDeleter() {
    bool deleted = false;
    {
        auto deleter = [&deleted](int* p) { deleted = true; delete p; };
        MyUniquePtr<int, decltype(deleter)> p(new int(1), deleter);
    }
    assert(deleted);
}

// shared_ptr tests
void testSharedPtrRefCount() {
    MySharedPtr<int> sp1(new int(10));
    assert(sp1.use_count() == 1);
    {
        MySharedPtr<int> sp2 = sp1;
        assert(sp1.use_count() == 2);
        assert(sp2.use_count() == 2);
    }
    assert(sp1.use_count() == 1);
}

// weak_ptr tests
void testWeakPtrLock() {
    MyWeakPtr<int> wp;
    {
        MySharedPtr<int> sp(new int(100));
        wp = sp;
        assert(!wp.expired());
        auto locked = wp.lock();
        assert(locked && *locked == 100);
    }
    assert(wp.expired());
    assert(wp.lock() == nullptr);
}

6.2 Edge Cases to Test

Test Expected Behavior
Move from null unique_ptr No crash, both null
Copy null shared_ptr No crash, both null
lock() expired weak_ptr Returns null shared_ptr
reset() on null No crash
Custom deleter called Exactly once
Self-assignment No crash, no double-free
Concurrent access No data races

7. Common Pitfalls & Debugging

Problem Symptom Root Cause Fix
Double-free Crash Two control blocks for same object Never create shared_ptr from raw pointer twice
Memory leak Growing memory Control block not deleted Check weak_count logic
Data race Inconsistent counts Non-atomic access Use std::atomic everywhere
Cyclic reference Leak Strong refs form cycle Use weak_ptr to break cycle
Use-after-free Crash or corruption weak_ptr dereference after expiry Always use lock()

Debugging Tips

  1. Add tracing: Print ref counts on every operation during development
  2. Use Valgrind: Check for leaks and invalid access
  3. Use ASan: Address Sanitizer catches use-after-free
  4. Use TSan: Thread Sanitizer catches data races
  5. Test single-threaded first: Get logic right before adding atomics

8. Extensions & Challenges

Extension Difficulty Concepts Learned
Array specialization: unique_ptr<T[]> Medium Template specialization
make_shared with single allocation Hard Placement new, alignment
enable_shared_from_this Hard CRTP pattern
Aliasing constructor Medium Pointing to member of shared object
intrusive_ptr (no control block) Hard Intrusive reference counting

9. Real-World Connections

How the Standard Library Implements This

Implementation Approach
libstdc++ (GCC) Virtual control block, combined allocation for make_shared
libc++ (Clang) Similar, with compressed pair for EBO
MSVC Split control block, separate weak block

Industry Patterns

  • Game engines often use custom smart pointers for specific memory pools
  • Chromium uses ref-counted pointers with custom semantics
  • Database engines use similar patterns for buffer pool management

10. Resources

Primary References

  • Meyers, S. “Effective Modern C++” - Items 18-22 (Smart Pointers)
  • Williams, A. “C++ Concurrency in Action” - Chapter 5 (Memory Model)
  • Stanford CS106L RAII Lecture

Online Resources


11. Self-Assessment Checklist

Before considering this project complete, verify:

  • unique_ptr: move works, copy fails to compile
  • unique_ptr: custom deleter works
  • unique_ptr: sizeof == sizeof(T*) for default deleter
  • shared_ptr: reference counting correct
  • shared_ptr: object deleted when last shared_ptr destroyed
  • weak_ptr: lock() returns valid shared_ptr when not expired
  • weak_ptr: lock() returns nullptr when expired
  • Control block deleted only when all weak_ptrs gone
  • Thread-safe: no races with concurrent operations
  • No memory leaks (Valgrind clean)

12. Submission / Completion Criteria

This project is complete when:

  1. All functional requirements (F1-F10) are implemented
  2. Example code from section 3.4 works correctly
  3. Thread-safety verified (no TSan warnings)
  4. Memory safety verified (no ASan/Valgrind errors)
  5. All unit tests pass
  6. Code compiles without warnings (-Wall -Wextra)
  7. You can explain the control block design
  8. You can explain memory ordering for atomics

Deliverables:

  • Header files for each smart pointer type
  • Comprehensive test file
  • Brief writeup explaining design decisions
  • Benchmark comparing to std:: versions

This project teaches the heart of modern C++ memory management. Understanding smart pointer internals makes you confident in using them correctly, debugging memory issues, and acing any C++ interview. The patterns here—RAII, move semantics, atomic reference counting—appear throughout systems programming.