Smart Pointers in C++: unique_ptr, shared_ptr, and weak_ptr

Feb 2, 2026 · C++

Manual memory management in C++ has long been a source of bugs, memory leaks, and undefined behavior. Smart pointers were introduced in C++11 to solve these problems by automating resource management. In this post, we'll explore the three main smart pointers: unique_ptr, shared_ptr, and weak_ptr, with practical examples of their use cases.

Why Smart Pointers Matter

Before smart pointers, C++ developers had to manually manage memory:

// Manual memory management - error-prone
void processData() {
    MyClass* obj = new MyClass();
    // ... do something with obj ...

    // What if we forget this?
    delete obj;  // Memory leak if exception thrown or early return
}

Smart pointers ensure automatic cleanup when objects go out of scope, eliminating memory leaks and making code exception-safe.

unique_ptr: Exclusive Ownership

std::unique_ptr represents exclusive ownership of a dynamically allocated object. Only one unique_ptr can own an object at a time, and ownership can be transferred but not shared.

Basic Usage

#include <memory>

std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::cout << *ptr << std::endl;  // Outputs: 42

// Automatic cleanup when ptr goes out of scope

Ownership Transfer

std::unique_ptr<int> ptr1 = std::make_unique<int>(100);

// Transfer ownership
std::unique_ptr<int> ptr2 = std::move(ptr1);

// ptr1 is now nullptr
assert(ptr1 == nullptr);
assert(*ptr2 == 100);

Use Cases for unique_ptr

1. Factory Functions

std::unique_ptr<Connection> createConnection(const std::string& url) {
    // Complex initialization logic
    auto conn = std::make_unique<Connection>();
    conn->connect(url);
    return conn;  // Ownership transferred to caller
}

void clientCode() {
    auto connection = createConnection("tcp://example.com:8080");
    // Use connection...
    // Automatic cleanup
}

2. Resource Management (RAII)

class FileHandler {
private:
    std::unique_ptr<FILE, decltype(&fclose)> file_;

public:
    FileHandler(const char* filename)
        : file_(fopen(filename, "r"), fclose) {
        if (!file_) {
            throw std::runtime_error("Failed to open file");
        }
    }

    void process() {
        // File automatically closed when FileHandler is destroyed
        char buffer[1024];
        while (fgets(buffer, sizeof(buffer), file_.get())) {
            // Process buffer
        }
    }
};

3. Polymorphism and Inheritance

class Animal { public: virtual void speak() = 0; virtual ~Animal() = default; };
class Dog : public Animal { public: void speak() override { std::cout << "Woof!\n"; } };
class Cat : public Animal { public: void speak() override { std::cout << "Meow!\n"; } };

std::unique_ptr<Animal> createPet(const std::string& type) {
    if (type == "dog") {
        return std::make_unique<Dog>();
    } else if (type == "cat") {
        return std::make_unique<Cat>();
    }
    return nullptr;
}

void petShow() {
    auto pet = createPet("dog");
    pet->speak();  // Outputs: Woof!
}

shared_ptr: Shared Ownership

std::shared_ptr enables shared ownership of objects. Multiple shared_ptr instances can point to the same object, and the object is destroyed only when the last shared_ptr is destroyed.

Reference Counting

shared_ptr uses reference counting to track how many pointers own the object:

std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::cout << ptr1.use_count() << std::endl;  // 1

std::shared_ptr<int> ptr2 = ptr1;
std::cout << ptr1.use_count() << std::endl;  // 2
std::cout << ptr2.use_count() << std::endl;  // 2

ptr2.reset();
std::cout << ptr1.use_count() << std::endl;  // 1

// Object destroyed when ptr1 goes out of scope

Use Cases for shared_ptr

1. Multiple Owners

class Observer {
public:
    virtual void update() = 0;
    virtual ~Observer() = default;
};

class Subject {
private:
    std::vector<std::shared_ptr<Observer>> observers_;

public:
    void addObserver(std::shared_ptr<Observer> observer) {
        observers_.push_back(observer);
    }

    void notify() {
        for (auto& obs : observers_) {
            obs->update();
        }
    }
};

class ConcreteObserver : public Observer {
public:
    void update() override {
        std::cout << "Observer notified\n";
    }
};

void observerPattern() {
    auto subject = std::make_shared<Subject>();
    auto observer1 = std::make_shared<ConcreteObserver>();
    auto observer2 = std::make_shared<ConcreteObserver>();

    subject->addObserver(observer1);
    subject->addObserver(observer2);

    subject->notify();  // Both observers get notified
    // All objects cleaned up automatically
}

2. Caches and Flyweights

class Texture {
public:
    Texture(const std::string& path) { /* load texture */ }
    // ...
};

class TextureManager {
private:
    std::unordered_map<std::string, std::weak_ptr<Texture>> cache_;

public:
    std::shared_ptr<Texture> getTexture(const std::string& path) {
        // Check if texture is already loaded
        if (auto cached = cache_[path].lock()) {
            return cached;
        }

        // Load new texture
        auto texture = std::make_shared<Texture>(path);
        cache_[path] = texture;
        return texture;
    }
};

3. Graph Structures

struct Node {
    int data;
    std::vector<std::shared_ptr<Node>> children;

    Node(int val) : data(val) {}
};

void buildTree() {
    auto root = std::make_shared<Node>(1);
    auto child1 = std::make_shared<Node>(2);
    auto child2 = std::make_shared<Node>(3);

    root->children.push_back(child1);
    root->children.push_back(child2);

    // All nodes automatically cleaned up
}

weak_ptr: Breaking Reference Cycles

std::weak_ptr is used to break circular references between shared_ptr objects. It doesn't contribute to the reference count and can only access the object if it still exists.

Basic Usage

std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;

std::cout << weak.use_count() << std::endl;  // 1 (only the shared_ptr counts)

// Access the object
if (auto locked = weak.lock()) {
    std::cout << *locked << std::endl;  // 42
} else {
    std::cout << "Object destroyed\n";
}

Use Cases for weak_ptr

1. Breaking Circular References

class Parent;
class Child;

class Parent {
public:
    std::shared_ptr<Child> child;
    ~Parent() { std::cout << "Parent destroyed\n"; }
};

class Child {
public:
    std::weak_ptr<Parent> parent;  // weak_ptr to break cycle
    ~Child() { std::cout << "Child destroyed\n"; }
};

void circularReference() {
    auto parent = std::make_shared<Parent>();
    auto child = std::make_shared<Child>();

    parent->child = child;
    child->parent = parent;  // weak_ptr doesn't create cycle

    // Objects are properly destroyed when they go out of scope
}

2. Observer Pattern (Safe Version)

class Subject;

class Observer {
public:
    virtual void update() = 0;
    virtual ~Observer() = default;
};

class Subject {
private:
    std::vector<std::weak_ptr<Observer>> observers_;  // weak_ptrs

public:
    void addObserver(std::shared_ptr<Observer> observer) {
        observers_.push_back(observer);  // weak_ptr from shared_ptr
    }

    void notify() {
        // Remove expired observers
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [](const std::weak_ptr<Observer>& wp) { return wp.expired(); }),
            observers_.end()
        );

        for (auto& wp : observers_) {
            if (auto obs = wp.lock()) {
                obs->update();
            }
        }
    }
};

3. Cache with Automatic Cleanup

class Cache {
private:
    std::unordered_map<std::string, std::weak_ptr<Data>> cache_;

public:
    void store(const std::string& key, std::shared_ptr<Data> data) {
        cache_[key] = data;
    }

    std::shared_ptr<Data> get(const std::string& key) {
        auto it = cache_.find(key);
        if (it != cache_.end()) {
            return it->second.lock();  // May return nullptr if expired
        }
        return nullptr;
    }

    void cleanup() {
        for (auto it = cache_.begin(); it != cache_.end(); ) {
            if (it->second.expired()) {
                it = cache_.erase(it);
            } else {
                ++it;
            }
        }
    }
};

Performance Considerations

Smart Pointer Overhead Use When
unique_ptr Zero runtime cost Single ownership, performance-critical code
shared_ptr Reference counting (atomic) Shared ownership, complex object graphs
weak_ptr Minimal (tracks shared_ptr) Breaking cycles, optional references

Best Practices

Common Pitfalls

Circular References

// BAD: Circular reference causes memory leak
class A { std::shared_ptr<B> b; };
class B { std::shared_ptr<A> a; };

// GOOD: Use weak_ptr to break the cycle
class A { std::shared_ptr<B> b; };
class B { std::weak_ptr<A> a; };

Premature Optimization

// Don't do this for performance reasons
std::unique_ptr<int> ptr(new int(42));  // Less efficient

// Do this instead
auto ptr = std::make_unique<int>(42);  // Better exception safety

Conclusion

Smart pointers are one of C++'s most important features for writing safe, maintainable code. Understanding when to use each type is crucial:

By using smart pointers appropriately, you can eliminate memory leaks, make your code exception-safe, and express ownership semantics clearly. Start with unique_ptr and only reach for shared_ptr when you truly need shared ownership.

d:\git_blog\iamtrusters.github.io\posts\2026-02-02-smart-pointers-cpp.html