Smart Pointers in C++
Use unique_ptr, shared_ptr, and weak_ptr to manage memory safely with clear ownership semantics.
Raw pointers in C++ give you full control but place the entire burden of memory management on you. Forgetting to call delete, deleting twice, or returning a pointer from a function that the caller doesn’t know it must free — these are the bugs that smart pointers eliminate. Since C++11, the standard library ships three smart pointer types that model different ownership patterns. The rule of thumb for modern C++: never write new or delete directly; always express ownership through a smart pointer.
Why Raw Pointers Cause Problems
Understanding what goes wrong with raw pointers makes it clear why smart pointers exist. The issue is not incompetence — even careful code has multiple paths that need cleanup, and exceptions make it worse.
// Every one of these paths is a potential memory leak or double-free
void risky(bool flag) {
int* p = new int(42);
if (flag) return; // leak: p is never deleted on this path
if (*p > 40) throw std::runtime_error("too big"); // leak: exception bypasses delete
delete p; // only reached on the happy path
}
Smart pointers solve this through RAII: the destructor runs no matter how you leave the scope — normal return, early return, or exception.
std::unique_ptr — Exclusive Ownership
unique_ptr is a zero-overhead wrapper that owns exactly one object. Its destructor calls delete automatically. It cannot be copied, only moved, which makes ownership transfer explicit — there is never any ambiguity about who owns the object.
#include <memory>
#include <iostream>
struct Connection {
Connection(std::string host) : host_(std::move(host)) {
std::cout << "Connected to " << host_ << "\n";
}
~Connection() { std::cout << "Disconnected from " << host_ << "\n"; }
void query(std::string sql) { /* ... */ }
private:
std::string host_;
};
void process() {
// Prefer make_unique — never write 'new' directly in application code
auto conn = std::make_unique<Connection>("db.example.com");
conn->query("SELECT 1");
// conn destroyed here automatically — disconnects even if an exception was thrown
}
// Transfer ownership with std::move — the caller loses the pointer
std::unique_ptr<Connection> open_connection(std::string host) {
return std::make_unique<Connection>(std::move(host));
}
int main() {
auto conn = open_connection("db.example.com");
// conn2 = conn; // compile error — copying unique_ptr is forbidden
auto conn2 = std::move(conn); // OK — ownership transferred, conn is now null
}
std::shared_ptr — Shared Ownership
shared_ptr uses reference counting: the object lives until the last shared owner is destroyed. Each copy of the shared_ptr increments the count; each destructor decrements it. Use shared_ptr when multiple parts of your program genuinely share ownership of an object and its lifetime is not naturally tied to any single one.
#include <memory>
#include <vector>
struct Image {
std::string path;
std::vector<uint8_t> pixels;
// expensive to copy — share ownership instead
};
// Multiple components can share the same image in memory without copying it
std::shared_ptr<Image> load_image(std::string path) {
return std::make_shared<Image>(Image{path, {/* pixels */}});
}
int main() {
auto img = load_image("background.png");
std::cout << img.use_count() << "\n"; // 1
{
auto thumb = img; // copy shared_ptr — both own the object, count becomes 2
std::cout << img.use_count() << "\n"; // 2
// thumb goes out of scope here — count drops to 1
}
std::cout << img.use_count() << "\n"; // 1 — object still alive
}
Use make_shared over shared_ptr<T>(new T): the former does one heap allocation for the object and its control block together. The latter does two, which wastes memory and creates an exception safety gap.
std::weak_ptr — Non-Owning Observer
weak_ptr observes a shared_ptr-managed object without contributing to the reference count. Its primary purpose is breaking reference cycles: if two objects each hold a shared_ptr to the other, neither count ever reaches zero and both leak. Making one direction weak_ptr breaks the cycle.
#include <memory>
#include <iostream>
struct Node {
int value;
std::shared_ptr<Node> next; // owns the next node
std::weak_ptr<Node> prev; // observes the previous node — no ownership
Node(int v) : value(v) {}
};
int main() {
auto n1 = std::make_shared<Node>(1);
auto n2 = std::make_shared<Node>(2);
n1->next = n2;
n2->prev = n1; // weak_ptr — does not extend n1's lifetime
// To use a weak_ptr, call lock() — returns a shared_ptr or nullptr
if (auto owner = n2->prev.lock()) {
std::cout << "prev value: " << owner->value << "\n"; // 1
}
} // n1 and n2 are correctly destroyed — no cycle, no leak
Custom Deleters
Both unique_ptr and shared_ptr accept a custom deleter for resources that aren’t plain heap objects — file handles, database connections, GPU resources, anything that needs a specific release function.
#include <memory>
#include <cstdio>
// FILE* managed by unique_ptr — fclose is called automatically
auto open_file(const char* path, const char* mode) {
return std::unique_ptr<FILE, decltype(&fclose)>(
fopen(path, mode),
&fclose // called instead of delete when unique_ptr is destroyed
);
}
// For shared_ptr, the deleter is type-erased (stored alongside the control block)
void manage_shared() {
std::shared_ptr<FILE> f(fopen("log.txt", "w"), &fclose);
fputs("hello\n", f.get());
}
Passing Smart Pointers to Functions
Getting ownership semantics right at function boundaries is where many C++ designs go wrong. The parameter type communicates intent clearly — use it to signal whether a function takes ownership, shares it, or just borrows.
// Takes ownership — caller loses the pointer, function is responsible for lifetime
void consume(std::unique_ptr<Widget> w);
// Borrows — function just uses the object, does not affect its lifetime
void use(Widget& w); // preferred: reference, can't be null
void use(Widget* w); // use when null is a valid input
// Shares ownership — function will keep the object alive beyond this call
void share(std::shared_ptr<Widget> w);
// Observes without extending lifetime — use when you just need to call methods
void observe(const std::shared_ptr<Widget>& w);
void caller() {
auto w = std::make_unique<Widget>();
use(*w); // pass underlying object — no ownership change
use(w.get()); // raw pointer — fine for non-owning use
consume(std::move(w)); // transfer ownership — w is null after this
}
Common Pitfalls
Shared_ptr from the same raw pointer twice creates two independent reference counts, both of which try to delete the object:
Widget* raw = new Widget();
std::shared_ptr<Widget> p1(raw);
std::shared_ptr<Widget> p2(raw); // UNDEFINED BEHAVIOR — double delete
// Always use make_shared or copy an existing shared_ptr
Circular shared_ptr references cause leaks because the count never reaches zero. Break cycles with weak_ptr.
Storing this in a shared_ptr inside a member function requires inheriting from std::enable_shared_from_this:
struct Session : std::enable_shared_from_this<Session> {
void async_read() {
auto self = shared_from_this(); // correct: shares ownership with callers
// pass self into lambda to keep Session alive during the async operation
}
};
Choosing the Right Pointer
| Situation | Use |
|---|---|
| Single owner, no sharing | unique_ptr |
| Shared ownership needed | shared_ptr |
| Observer that must not extend lifetime | weak_ptr |
| Non-owning parameter to a function | raw pointer or reference |
| Transfer ownership into a function | unique_ptr by value |