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;
}
Output:
Task::Constructor
23
Task::Destructor
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;
}
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
};
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;
}
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