Modern C++ Smart Pointers: unique_ptr and shared_ptr
Use unique_ptr, shared_ptr, and weak_ptr to model ownership in modern C++ without manual new and delete or memory leaks.
What you'll learn
- ✓Model sole ownership with unique_ptr
- ✓Share ownership safely with shared_ptr
- ✓Break reference cycles using weak_ptr
- ✓Prefer make_unique and make_shared
- ✓Pass smart pointers across function boundaries correctly
Prerequisites
- •Pointers and memory — see /blog/cpp-pointers-and-memory
Smart pointers are the modern C++ answer to manual new/delete. They are class templates that own a heap allocation and free it automatically when they go out of scope. Used consistently, they eliminate leaks, double frees, and most dangling pointer bugs.
unique_ptr — sole ownership
std::unique_ptr<T> owns one heap object. It cannot be copied, only moved. When it dies, the object dies with it.
#include <memory>
auto p = std::make_unique<int>(42);
std::cout << *p; // 42
// no delete needed
std::make_unique is preferred over std::unique_ptr<int>(new int(42)). It is exception-safe and shorter.
Transferring ownership
A unique_ptr moves but does not copy. This expresses transfer of ownership in the type system.
auto a = std::make_unique<int>(1);
auto b = std::move(a); // b owns the int; a is now empty
// auto c = a; // error — no copy
After std::move, the source is non-null only if you reassign it. Always reason about a moved-from unique_ptr as empty.
unique_ptr in function signatures
- Take
std::unique_ptr<T>by value when you want the function to take ownership. - Take
const T&orT*when you only need to read or use the object.
void store(std::unique_ptr<Widget> w); // takes ownership
void render(const Widget& w); // borrow, no ownership
Returning a unique_ptr is the natural way to express “factory returns a fresh allocation.”
std::unique_ptr<Widget> make_widget() {
return std::make_unique<Widget>();
}
shared_ptr — shared ownership
std::shared_ptr<T> allows multiple owners. It maintains a reference count; when the last shared_ptr to the object dies, the object is freed.
auto a = std::make_shared<int>(7);
auto b = a; // both own the int
std::cout << a.use_count(); // 2
std::make_shared is faster than the explicit constructor because it allocates the object and the control block in one block.
When to reach for shared_ptr: when ownership is genuinely shared and you cannot identify a single owner. Often you can — reach for unique_ptr first.
weak_ptr — non-owning observer
A std::weak_ptr<T> watches a shared_ptr without contributing to the count. Call lock() to attempt to obtain a shared_ptr; you get a null one if the object is gone.
std::shared_ptr<int> owner = std::make_shared<int>(10);
std::weak_ptr<int> watcher = owner;
if (auto p = watcher.lock()) {
std::cout << *p;
} else {
std::cout << "expired";
}
The main use case: breaking reference cycles. Two shared_ptrs pointing at each other never reach zero and leak. Replace one with weak_ptr to break the cycle.
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // break the cycle
};
Custom deleters
Smart pointers can free non-heap resources by accepting a deleter.
auto file = std::unique_ptr<FILE, decltype(&std::fclose)>(
std::fopen("data.txt", "r"), &std::fclose);
if (file) {
char buf[256];
std::fgets(buf, sizeof(buf), file.get());
}
When file leaves scope, fclose runs. The same pattern wraps C APIs that have paired create/destroy functions.
Smart pointers in containers
unique_ptr is movable, which is enough for std::vector to store it. This is the modern way to hold polymorphic objects.
#include <vector>
#include <memory>
struct Shape { virtual double area() const = 0; virtual ~Shape() = default; };
struct Circle : Shape { /* ... */ };
struct Square : Shape { /* ... */ };
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Square>());
for (const auto& s : shapes) std::cout << s->area();
When the vector is destroyed, every shape is freed. No leak, no manual loop.
Avoiding common bugs
Never construct two smart pointers from the same raw pointer:
int* raw = new int(1);
std::unique_ptr<int> a(raw);
std::unique_ptr<int> b(raw); // both will delete — double free
Stay inside make_unique and make_shared and you cannot make this mistake.
When passing to functions that take a raw pointer, use .get():
void use(int* p);
auto p = std::make_unique<int>(1);
use(p.get()); // borrow, do not transfer
The function must not free or take ownership of the pointer.
Performance notes
unique_ptrhas the same size and overhead as a raw pointer. There is no excuse to skip it for performance.shared_ptris two pointers wide and pays a small atomic cost on copy/destruction. Fine for most code; avoid in tight inner loops.make_shareddoes one allocation instead of two — usually faster than the explicit form.
A worked example
A small resource handle hierarchy:
#include <memory>
#include <string>
class Logger {
public:
explicit Logger(std::string name) : name_(std::move(name)) {}
void log(const std::string& msg) const {
std::cout << '[' << name_ << "] " << msg << '\n';
}
private:
std::string name_;
};
class Service {
public:
explicit Service(std::shared_ptr<Logger> log) : log_(std::move(log)) {}
void run() { log_->log("running"); }
private:
std::shared_ptr<Logger> log_;
};
int main() {
auto log = std::make_shared<Logger>("main");
Service a(log), b(log);
a.run();
b.run();
} // Logger destroyed automatically when last Service is gone
Ownership is shared explicitly; lifetime is automatic.
Where this fits
Smart pointers are the cleanest way to apply the Rule of Zero to heap-owned data. They also fit naturally into STL Containers, letting you store polymorphic objects without leaks.
What is next
Round out modern C++ with auto, range-for, and structured bindings. Then revisit the rest of the standard library with confidence that ownership is handled.
Wrap up
Default to unique_ptr and prove you need shared_ptr before reaching for it. Use make_unique and make_shared always. Pass by raw pointer or reference when borrowing; pass by smart pointer when transferring ownership. Manual delete should be a code smell in your reviews.