Smart Pointers

Automatic memory management with unique_ptr, shared_ptr, and weak_ptr

The Raw Pointer Problem

// Raw pointer issues:
// 1. Ownership unclear
// 2. Manual delete required
// 3. Exception safety problems
// 4. Easy to leak or double-free

void process() {
    Resource* r = new Resource();
    
    if (some_condition()) {
        return;  // Leak! delete never called
    }
    
    if (other_condition()) {
        throw std::runtime_error("error");  // Leak!
    }
    
    delete r;  // Easy to forget
}

std::unique_ptr

Exclusive ownership. Lightweight, zero overhead. The go-to smart pointer.

#include <memory>

// Creating unique_ptr
auto ptr = std::make_unique<Resource>();  // Preferred
std::unique_ptr<Resource> ptr2(new Resource());  // Avoid direct new

// Automatic cleanup
void process() {
    auto r = std::make_unique<Resource>();
    
    if (some_condition()) {
        return;  // Resource freed automatically
    }
    
    throw std::runtime_error("error");  // Resource freed automatically
}  // Resource freed when ptr goes out of scope

// Transfer ownership
auto ptr2 = std::move(ptr);  // ptr is now empty
// ptr->doSomething();  // ERROR: nullptr dereference

// Pass to functions
void use_resource(const Resource* r);     // Borrow
void take_resource(std::unique_ptr<Resource> r);  // Take ownership
void maybe_modify(std::unique_ptr<Resource>& r);  // Might modify

std::shared_ptr

Shared ownership with reference counting. Use when multiple owners need the object.

// Creating shared_ptr
auto ptr = std::make_shared<Resource>();  // Preferred (single allocation)
std::shared_ptr<Resource> ptr2(new Resource());  // Two allocations

// Shared ownership
{
    auto ptr1 = std::make_shared<Resource>();
    {
        auto ptr2 = ptr1;  // Reference count = 2
        {
            auto ptr3 = ptr1;  // Reference count = 3
        }  // Reference count = 2
    }  // Reference count = 1
}  // Reference count = 0, Resource destroyed

// Custom deleter
auto file = std::shared_ptr<FILE>(
    fopen("data.txt", "r"),
    [](FILE* f) { if (f) fclose(f); }
);

std::weak_ptr

Non-owning "observer" pointer. Breaks circular references.

// Problem: Circular reference causes leak
struct Node {
    std::shared_ptr<Node> next;
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a;  // Memory leak! Neither will be destroyed

// Solution: weak_ptr
struct Node {
    std::weak_ptr<Node> next;  // Doesn't keep next alive
    
    void process() {
        if (auto shared = next.lock()) {  // Convert to shared_ptr
            // shared is valid, use it
        } else {
            // Node was destroyed
        }
    }
};

Comparison Table

Featureunique_ptrshared_ptrweak_ptr
OwnershipExclusiveSharedNone (observes)
CopyableNo (move only)YesYes
OverheadNoneReference count (atomic)Minimal
Use forSingle ownershipShared ownershipBreaking cycles

Factory Functions

// std::make_unique (C++14)
auto ptr = std::make_unique<MyClass>(arg1, arg2);
// Benefits:
// - Exception safe
// - Single allocation
// - Cleaner syntax

// std::make_shared (C++11)
auto ptr = std::make_shared<MyClass>(arg1, arg2);
// Benefits:
// - Single allocation (object + control block together)
// - Better cache locality
// - Exception safe

// Avoid direct new:
std::unique_ptr<MyClass> bad(new MyClass());  // Exception unsafe
std::shared_ptr<MyClass> bad2(new MyClass());  // Two allocations

Best Practices

  • Default to std::unique_ptr for ownership
  • Use std::make_unique and std::make_shared
  • Pass unique_ptr by value to transfer ownership
  • Pass shared_ptr by const reference if not taking ownership
  • Use weak_ptr to break circular references
  • Don't use new/delete directly
  • Don't create shared_ptr from raw pointer multiple times