Smart Pointers: Modern C++ Memory Management

Why Smart Pointers?

Smart pointers are like responsible pet owners - they automatically take care of their resources, feeding them when needed and cleaning up after them. No more memory leaks or dangling pointers!

The Smart Pointer Family

graph TD A[Smart Pointers] --> B[unique_ptr] A --> C[shared_ptr] A --> D[weak_ptr] B --> E[Exclusive ownership
Most common
Zero overhead] C --> F[Shared ownership
Reference counted
Thread-safe counting] D --> G[Observes shared_ptr
Breaks cycles
Can expire] style B fill:#E3F2FD style C fill:#E8F5E9 style D fill:#FFF3E0

unique_ptr: Exclusive Ownership

unique_ptr is like having a single key to a house - only one person can own it at a time. When they're done, the house is automatically sold (memory freed)!

unique_ptr Ownership Transfer Step 1: Creation unique_ptr p1 Object Step 2: After Move nullptr p1 Object unique_ptr p2 Ownership transferred!

unique_ptr Examples

#include 
#include 
using namespace std;

// Basic unique_ptr usage
void uniquePtrBasics() {
    // Creating unique_ptr
    unique_ptr p1(new int(42));           // Direct initialization
    auto p2 = make_unique(42);            // Preferred: make_unique
    
    // Accessing the value
    cout << *p2 << endl;                       // Dereference: 42
    cout << p2.get() << endl;                  // Get raw pointer
    
    // unique_ptr with arrays
    auto arr = make_unique(10);         // Array of 10 ints
    arr[0] = 100;
    
    // Custom deleter
    auto fileDeleter = [](FILE* f) { 
        if (f) fclose(f); 
    };
    unique_ptr file(
        fopen("data.txt", "r"), fileDeleter
    );
}

// Ownership transfer
unique_ptr createMessage() {
    auto msg = make_unique("Hello from function!");
    return msg;  // Automatic move
}

void transferOwnership() {
    auto p1 = make_unique(42);
    // unique_ptr p2 = p1;             // ERROR: Can't copy
    unique_ptr p2 = move(p1);         // OK: Move ownership
    
    if (!p1) {
        cout << "p1 is now null" << endl;
    }
    
    // Reset and release
    p2.reset();                             // Delete object, set to null
    p2.reset(new int(100));                 // Delete old, own new
    
    int* raw = p2.release();                // Release ownership
    delete raw;                             // Now we must delete manually
}

// Using unique_ptr in classes
class Resource {
private:
    unique_ptr data;
    size_t size;
    
public:
    Resource(size_t n) : data(make_unique(n)), size(n) {
        // Initialize array
        for (size_t i = 0; i < size; ++i) {
            data[i] = i;
        }
    }
    
    // No need for destructor - unique_ptr handles it!
    // No need for copy constructor/assignment - deleted by default
    
    // Move constructor (compiler-generated is fine)
    Resource(Resource&&) = default;
    Resource& operator=(Resource&&) = default;
    
    int& operator[](size_t i) { return data[i]; }
};

shared_ptr: Shared Ownership

shared_ptr is like a shared apartment - multiple roommates can have keys. The apartment is only sold when the last roommate moves out!

shared_ptr Examples

// Basic shared_ptr usage
void sharedPtrBasics() {
    // Creating shared_ptr
    shared_ptr sp1(new int(42));           // Direct
    auto sp2 = make_shared(42);            // Preferred: more efficient
    
    // Copying increases reference count
    shared_ptr sp3 = sp2;                  // Count: 2
    {
        shared_ptr sp4 = sp2;              // Count: 3
    }                                            // Count: 2 (sp4 destroyed)
    
    // Check reference count
    cout << "Use count: " << sp2.use_count() << endl;
    
    // Check if unique owner
    if (sp1.unique()) {
        cout << "sp1 is the only owner" << endl;
    }
}

// Circular reference problem
class Node {
public:
    int value;
    shared_ptr next;
    shared_ptr prev;    // Creates cycle!
    
    Node(int val) : value(val) {
        cout << "Node " << value << " created" << endl;
    }
    
    ~Node() {
        cout << "Node " << value << " destroyed" << endl;
    }
};

void circularReferenceProblem() {
    auto node1 = make_shared(1);
    auto node2 = make_shared(2);
    
    node1->next = node2;
    node2->prev = node1;    // Circular reference!
    
    // When function ends, nodes are NOT destroyed
    // Reference count never reaches 0
}

// Solution: Use weak_ptr
class SafeNode {
public:
    int value;
    shared_ptr next;
    weak_ptr prev;    // Weak reference breaks cycle
    
    SafeNode(int val) : value(val) {}
};

// Custom deleter with shared_ptr
void customDeleterExample() {
    auto arrayDeleter = [](int* p) {
        cout << "Deleting array" << endl;
        delete[] p;
    };
    
    shared_ptr sp(new int[10], arrayDeleter);
    
    // Or use default_delete for arrays
    shared_ptr arr(new int[10]);
}

// Aliasing constructor
struct Person {
    string name;
    int age;
};

void aliasingExample() {
    auto person = make_shared(Person{"Alice", 30});
    
    // Create shared_ptr to member
    shared_ptr namePtr(person, &person->name);
    
    // namePtr keeps person alive even if person goes out of scope
}

weak_ptr: Breaking Cycles

weak_ptr is like having a business card - you can call the number to check if the business still exists, but the card itself doesn't keep the business running!

weak_ptr Lifecycle T1 Create shared_ptr T2 Create weak_ptr T3 shared_ptr destroyed T4 weak_ptr expired Object exists weak_ptr can lock() Object gone lock() returns null

weak_ptr Examples

// Basic weak_ptr usage
void weakPtrBasics() {
    shared_ptr sp = make_shared(42);
    weak_ptr wp = sp;                      // Create weak_ptr
    
    // Check if still valid
    if (!wp.expired()) {
        // Convert to shared_ptr to use
        if (auto locked = wp.lock()) {
            cout << "Value: " << *locked << endl;
            // locked keeps object alive in this scope
        }
    }
    
    sp.reset();                                  // Destroy object
    
    if (wp.expired()) {
        cout << "Object has been destroyed" << endl;
    }
}

// Observer pattern with weak_ptr
class Subject;

class Observer {
private:
    weak_ptr subject;
    
public:
    void observe(shared_ptr s) {
        subject = s;
    }
    
    void update() {
        if (auto s = subject.lock()) {
            // Subject still exists, can safely use
            cout << "Updating based on subject" << endl;
        } else {
            cout << "Subject has been destroyed" << endl;
        }
    }
};

// Tree structure with parent pointers
class TreeNode : public enable_shared_from_this {
private:
    int value;
    weak_ptr parent;                  // Weak to avoid cycle
    vector> children;       // Strong ownership
    
public:
    TreeNode(int val) : value(val) {}
    
    void addChild(shared_ptr child) {
        children.push_back(child);
        child->parent = shared_from_this();      // Get shared_ptr to this
    }
    
    shared_ptr getParent() {
        return parent.lock();                    // May return nullptr
    }
};

// Cache with automatic cleanup
template
class Cache {
private:
    map> cache;
    
public:
    void insert(const Key& key, shared_ptr value) {
        cache[key] = value;
    }
    
    shared_ptr get(const Key& key) {
        auto it = cache.find(key);
        if (it != cache.end()) {
            if (auto value = it->second.lock()) {
                return value;                    // Still valid
            } else {
                cache.erase(it);                // Clean up expired entry
            }
        }
        return nullptr;
    }
    
    void cleanup() {
        for (auto it = cache.begin(); it != cache.end(); ) {
            if (it->second.expired()) {
                it = cache.erase(it);
            } else {
                ++it;
            }
        }
    }
};

Smart Pointer Best Practices

graph TD A[Smart Pointer Guidelines] --> B[Prefer make_unique/make_shared] A --> C[Use unique_ptr by default] A --> D[shared_ptr only when needed] A --> E[weak_ptr to break cycles] B --> F[Exception safe
Single allocation
More efficient] C --> G[Zero overhead
Clear ownership
Move semantics] D --> H[Multiple owners
Thread-safe counting
Higher overhead] E --> I[Observer pattern
Caches
Back pointers]

Common Pitfalls and Solutions

Performance Considerations

// Performance comparison
class PerformanceDemo {
public:
    // unique_ptr: Zero overhead
    void uniquePtrPerf() {
        unique_ptr up(new int(42));
        // Same performance as raw pointer
        // No reference counting
        // Inline destructor
    }
    
    // shared_ptr: Reference counting overhead
    void sharedPtrPerf() {
        shared_ptr sp(new int(42));
        // Atomic reference counting (thread-safe)
        // Control block allocation
        // Virtual destructor call
    }
    
    // make_shared optimization
    void makeSharedOptimization() {
        // Two allocations: object + control block
        shared_ptr sp1(new int(42));
        
        // Single allocation: object + control block together
        auto sp2 = make_shared(42);  // More cache-friendly
    }
    
    // Moving vs copying
    void moveVsCopy() {
        auto sp1 = make_shared();
        
        // Copying: Atomic increment/decrement
        shared_ptr sp2 = sp1;          // Slower
        
        // Moving: No atomic operations
        shared_ptr sp3 = move(sp1);    // Faster
    }
};

// Custom allocators
template
class PoolAllocator {
    // Custom allocation strategy
};

void customAllocatorExample() {
    // With custom allocator
    allocator alloc;
    shared_ptr sp(
        allocate_shared(alloc, 42)
    );
}

Practice Exercise: Resource Manager

Build a Resource Management System

Create a system that manages various resources using smart pointers:

  1. File handles with automatic closing
  2. Thread pool with shared ownership
  3. Object pool with weak references
  4. Circular buffer with proper cleanup
class FileHandle {
private:
    FILE* file;
    
public:
    FileHandle(const string& filename, const string& mode);
    ~FileHandle();
    // TODO: Make it work with unique_ptr
};

class ThreadPool {
private:
    vector> workers;
    queue> tasks;
    
public:
    void enqueue(function task);
    // TODO: Implement with proper synchronization
};

template
class ObjectPool {
private:
    stack> available;
    vector> inUse;
    
public:
    shared_ptr acquire();
    void release(shared_ptr obj);
    // TODO: Implement recycling mechanism
};
Implementation Hints

Smart Pointers and Containers

Smart Pointers in STL Containers vector<unique_ptr<T>> Move-only container, efficient polymorphic storage vector<shared_ptr<T>> Shared ownership, safe copying, higher overhead map<Key, weak_ptr<T>> Cache patterns, automatic cleanup of expired entries

Container Usage Examples

// Polymorphic container with unique_ptr
class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual void draw() const = 0;
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override { return 3.14159 * radius * radius; }
    void draw() const override { cout << "Drawing circle" << endl; }
};

void polymorphicContainer() {
    vector> shapes;
    
    // Add different shapes
    shapes.push_back(make_unique(5.0));
    shapes.push_back(make_unique(4.0, 6.0));
    
    // Can't copy the vector, but can move it
    vector> newShapes = move(shapes);
    
    // Process all shapes polymorphically
    for (const auto& shape : newShapes) {
        shape->draw();
        cout << "Area: " << shape->area() << endl;
    }
}

// Shared ownership in containers
class Task {
    string name;
    function work;
public:
    Task(string n, function w) : name(n), work(w) {}
    void execute() { work(); }
};

class TaskManager {
    vector> pendingTasks;
    vector> runningTasks;
    
public:
    void addTask(shared_ptr task) {
        pendingTasks.push_back(task);
    }
    
    void startTask(size_t index) {
        if (index < pendingTasks.size()) {
            // Task can be in both vectors
            runningTasks.push_back(pendingTasks[index]);
        }
    }
};

Modern C++ Features

graph TD A[C++11] --> B[unique_ptr
shared_ptr
weak_ptr] C[C++14] --> D[make_unique] E[C++17] --> F[Array support
improved] G[C++20] --> H[make_shared for arrays
atomic operations] style A fill:#E3F2FD style C fill:#E8F5E9 style E fill:#FFF3E0 style G fill:#F3E5F5

Challenge Exercise: Memory Pool

Advanced Smart Pointer Challenge

Implement a thread-safe memory pool using smart pointers:

  1. Pre-allocate objects for performance
  2. Recycle objects automatically
  3. Thread-safe allocation/deallocation
  4. Weak references for monitoring
  5. Statistics tracking
Design Hints

Key Takeaways

graph LR A[Master Smart Pointers] --> B[Write Safe Code] B --> C[Eliminate Memory Leaks] C --> D[Build Robust Systems] D --> E[Modern C++ Expert!]