DEV Community

Cover image for C++ Smart Pointers, unique_ptr vs shared_ptr vs weak_ptr — How to Choose
elysianx
elysianx

Posted on

C++ Smart Pointers, unique_ptr vs shared_ptr vs weak_ptr — How to Choose

When I first learned about smart pointers, I was equally confused by all three of them! They are used to manage memory dynamically. unique_ptr is like a master who is responsible for its pet's life. shared_ptr is like you and your friends renting a house together. weak_ptr is like a bystander who can only watch. This post explains what each one really does, with short examples.

Quick answer: use unique_ptr by default. Use shared_ptr only when ownership is truly shared. Use weak_ptr when you need to observe without owning.

unique_ptr shared_ptr weak_ptr
Ownership model exclusive ownership shared ownership no ownership
Can it be copied No Yes Yes
Can it have multiple owners No Yes No (it owns nothing)
Reference counting overhead none atomic count increase/decrease on copy and destroy touches a separate weak count, never the strong count
How to choose the default choice when ownership is truly shared to break circular references

Unique_ptr

unique_ptr means: "I'm your master and responsible for your life and death."

Example:

#include <iostream>
#include <memory>
using namespace std;

struct Task {
    int mId;
    Task(int id) :mId(id) {
        cout << "Task::Constructor" << endl;
    }
    ~Task() {
        cout << "Task::Destructor" << endl;
    }
};

int main()
{
    unique_ptr<Task> taskPtr(new Task(23));
    int id = taskPtr->mId;

    cout << id << endl;

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Output:

Task::Constructor
23
Task::Destructor
Enter fullscreen mode Exit fullscreen mode

Why unique_ptr can't be copied

unique_ptr can't be copy-constructed or copy-assigned, because copying would let two unique_ptrs point to the same object — and when both are destroyed, the object would be deleted twice. Instead, you use std::move to transfer ownership.

More on ownership transfer: see my earlier post, reset() vs release() vs std::move.

When to choose unique_ptr

  • unique_ptr prevents memory leaks caused by programming errors — the object is freed automatically when the pointer goes out of scope, even if you forget to delete.
  • Ownership can be transferred cheaply with std::move, without copying the managed object.

Shared_ptr

shared_ptr means: the house is rented together by friends. When one friend moves out, the house still belongs to the rest — the object is destroyed only when the last owner releases it.

Example:

#include <iostream>
#include <memory>

class House {
public:
    House()  { std::cout << "That's a big house." << std::endl; }
    ~House() { std::cout << "Bye" << std::endl; }
};

int main() {
    std::shared_ptr<House> friend1 = std::make_shared<House>();  // one allocation: object + control block
    std::shared_ptr<House> friend2 = friend1;  // a roommate moves in

    std::cout << friend1.use_count() << std::endl;  // 2
    std::cout << friend2.use_count() << std::endl;  // 2

    friend1.reset();  // a friend moves out
    std::cout << friend2.use_count() << std::endl;  // 1
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Each shared_ptr copy increments the reference count; each destruction or reset() decrements it. When the count reaches zero, the object is destroyed.

Thread safety of shared_ptr

The member functions of shared_ptr (including the copy constructor and copy assignment operator) can be called on different shared_ptr instances from multiple threads at the same time without any extra synchronization. However, if multiple threads access the same shared_ptr instance and at least one of them calls a non-const member function, a data race occurs. To share the same instance safely, use std::atomic<std::shared_ptr<T>> (since C++20). In C++11/14/17, use the free functions std::atomic_load() and std::atomic_store() instead.

The trap of shared_ptr: circular references

shared_ptr has one famous trap: circular references. If two objects own each other with shared_ptr, neither reference count can ever reach zero — the memory leaks even after you drop all external references. A doubly-linked structure shows the shape of the problem:

#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // must NOT be shared_ptr — otherwise two nodes
                               // keep each other alive forever and leak
};
Enter fullscreen mode Exit fullscreen mode

Use weak_ptr for the "back" direction. It breaks the cycle: the object is freed when no one strongly owns it, and the back-pointer simply expires.


Weak_ptr

weak_ptr means: I'm a bystander. I can observe you, but I'm not responsible for you.

Example:

#include <iostream>
#include <memory>

class Object {
public:
    Object(int value) : data(value) {
        std::cout << "Object created with value: " << data << std::endl;
    }
    ~Object() {
        std::cout << "Object destroyed with value: " << data << std::endl;
    }
    int data;
};

int main() {
    std::shared_ptr<Object> sharedObjectA = std::make_shared<Object>(42);
    std::weak_ptr<Object> weakObjectA = sharedObjectA;

    if (auto sp = weakObjectA.lock()) {  // safe idiom: check and lock in one step
        std::cout << "Value: " << sp->data << std::endl;
    } else {
        std::cout << "Object is gone" << std::endl;
    }

    sharedObjectA.reset();
    std::cout << "End of the Program" << std::endl;
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Key member functions

  • lock(): if the object is still alive, returns a temporary shared_ptr; if it has been destroyed, returns an empty shared_ptr. Always use if (auto sp = w.lock()) instead of calling expired() and then lock() separately — the object could be destroyed between the two calls.
  • expired(): checks whether the managed object has already been destroyed.
  • reset(): releases the weak reference.
  • swap(): swaps the managed object with another weak_ptr.

Application

  • Breaking circular references: when an object needs to reference another object without owning it — like Node::prev above — use weak_ptr. The cycle is broken, and both objects are safely released when no longer needed.
  • Caching systems: caches often hold weak_ptrs so they don't keep objects alive; when an object is no longer used anywhere else, the cache entry simply expires instead of leaking.

Q&A

Q: Can a unique_ptr be converted into a shared_ptr?
A: Yes, and it's the only "copy-like" conversion allowed — it actually moves the ownership: std::shared_ptr<T> sp(std::move(up));. The reference count starts at 1 and nobody has to worry about double deletion.

Q: Is shared_ptr thread-safe?
A: Partly. The reference counting is atomic, so copies and destructions on different instances are safe. But the pointed-to object itself is not protected — if two threads read and write the object's data through their shared_ptrs, you still need a mutex or an atomic.

Q: Can I copy a unique_ptr?
A: No, by design — copying would cause the object to be deleted twice. If you really need to transfer it, std::move it.


Reference

Top comments (0)