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_relaxedfor increment,memory_order_acq_relfor 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
newanddelete” - 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/deleteeverywhere - Smart pointers existed (auto_ptr, Boost) but were limited
- Memory leaks and dangling pointers were common bugs
C++11 introduced:
unique_ptr(replacing brokenauto_ptr)shared_ptrandweak_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:
MyUniquePtr<T>- Exclusive ownership smart pointerMySharedPtr<T>- Shared ownership with reference countingMyWeakPtr<T>- Non-owning observer that can lock to shared_ptr- Custom deleter support for both unique and shared pointers
makeUnique<T>andmakeShared<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:
- How do I tie resource cleanup to object destruction (RAII)?
- How do I prevent copying for exclusive ownership (unique_ptr)?
- How do I share ownership among multiple pointers (shared_ptr)?
- How do I observe without owning (weak_ptr)?
- 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:
- After (1): strong=1, weak=1, Widget exists
- After (2): strong=2, weak=1, Widget exists
- After (3): strong=2, weak=2, Widget exists
- After (4): strong=1, weak=2, Widget exists (sp2 still owns)
- After (5): strong=2, weak=2 (sp3 locked successfully)
- 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
- “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
- “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)
- “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)
- “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
- “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
- Add tracing: Print ref counts on every operation during development
- Use Valgrind: Check for leaks and invalid access
- Use ASan: Address Sanitizer catches use-after-free
- Use TSan: Thread Sanitizer catches data races
- 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:
- All functional requirements (F1-F10) are implemented
- Example code from section 3.4 works correctly
- Thread-safety verified (no TSan warnings)
- Memory safety verified (no ASan/Valgrind errors)
- All unit tests pass
- Code compiles without warnings (-Wall -Wextra)
- You can explain the control block design
- 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.