C++ course menu
C++ Smart Pointers: unique_ptr, shared_ptr and weak_ptr
Learn modern C++ smart pointers: std::unique_ptr for single ownership with make_unique, moving ownership, std::shared_ptr with reference counting and make_shared, std::weak_ptr to break cycles, and how to choose the right pointer.
On this page
A smart pointer is an object that holds a pointer and owns what it points to: when the smart pointer is destroyed, it frees the memory automatically. Smart pointers (from <memory>) give you dynamic memory without leaks, double deletes or forgotten delete calls.
There are three:
unique_ptr: one owner#
Create one with std::make_unique. Use it like a normal pointer, with * and ->:
#include <iostream>
#include <memory>
#include <string>
struct Player {
std::string name;
int score = 0;
explicit Player(std::string n) : name(std::move(n)) {
std::cout << "Player " << name << " created\n";
}
~Player() {
std::cout << "Player " << name << " destroyed\n";
}
};
int main() {
{
std::unique_ptr<Player> p = std::make_unique<Player>("Asha");
p->score += 10;
std::cout << p->name << " has " << p->score << " points\n";
} // p goes out of scope: the Player is deleted automatically
std::cout << "end of main\n";
}Player Asha created
Asha has 10 points
Player Asha destroyed
end of mainNo delete anywhere, and no leak, even if an exception were thrown inside the block. A unique_ptr is as small and fast as a raw pointer.
Moving ownership#
A unique_ptr cannot be copied, because then two owners would both try to delete the object. Ownership can be moved with std::move, leaving the source empty:
#include <iostream>
#include <memory>
#include <utility>
int main() {
auto a = std::make_unique<int>(99);
// auto b = a; // error: copying is not allowed
auto b = std::move(a); // ownership moves to b
std::cout << std::boolalpha << (a == nullptr) << ' ' << *b << '\n';
}true 99Functions use this to express ownership clearly: returning a unique_ptr hands ownership to the caller, and taking one by value takes ownership from the caller. Move Semantics explains std::move in depth.
unique_ptr in containers#
A std::vector<std::unique_ptr<T>> owns a list of heap objects, a very common pattern for collections of polymorphic objects (see Polymorphism):
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Task {
std::string title;
};
int main() {
std::vector<std::unique_ptr<Task>> tasks;
tasks.push_back(std::make_unique<Task>(Task{"Write report"}));
tasks.push_back(std::make_unique<Task>(Task{"Review code"}));
for (const auto& t : tasks) {
std::cout << t->title << '\n';
}
} // the vector destroys every unique_ptr, which deletes every TaskWrite report
Review codeshared_ptr: shared ownership#
Sometimes several parts of a program need the same object, and there is no single obvious owner. A std::shared_ptr keeps a reference count: copying it adds an owner, destroying a copy removes one, and the object is deleted when the count reaches zero:
#include <iostream>
#include <memory>
#include <string>
struct Document {
std::string title;
explicit Document(std::string t) : title(std::move(t)) {}
~Document() { std::cout << "Document closed\n"; }
};
int main() {
auto doc = std::make_shared<Document>("Plan");
std::cout << "owners: " << doc.use_count() << '\n';
{
std::shared_ptr<Document> editor = doc;
std::cout << "owners: " << doc.use_count() << '\n';
}
std::cout << "owners: " << doc.use_count() << '\n';
}owners: 1
owners: 2
owners: 1
Document closedshared_ptr costs more than unique_ptr: it allocates a control block and updates the count (thread-safely) on every copy. Use it only when ownership really is shared.
weak_ptr: observing without owning#
Two objects that hold shared_ptrs to each other form a cycle: neither count ever reaches zero, and both leak. A std::weak_ptr refers to a shared object without owning it. To use it, call lock(), which returns a shared_ptr (empty if the object is already gone):
#include <iostream>
#include <memory>
int main() {
std::weak_ptr<int> observer;
{
auto value = std::make_shared<int>(7);
observer = value;
if (auto locked = observer.lock()) {
std::cout << "still alive: " << *locked << '\n';
}
}
std::cout << std::boolalpha << "expired: " << observer.expired() << '\n';
}still alive: 7
expired: trueTypical uses are a child pointing back to its parent, caches, and observers.
Which pointer should I use?#
- Can it be a plain object or a container (
std::vector,std::string)? Do that. No pointer needed. - Does it need to live on the heap with one clear owner?
std::unique_ptr. - Is ownership genuinely shared?
std::shared_ptr, withstd::weak_ptrto break cycles. - Just need to look at an object someone else owns? A reference, or a raw pointer if it can be null.
Always create smart pointers with std::make_unique and std::make_shared rather than new.
Common mistakes#
Practice#
Practice exercise
Write a function std::unique_ptr<std::vector<int>> make_range(int n) that returns a heap-allocated vector containing 1 to n. In main, print the sum of make_range(10).
Show solution
#include <iostream>
#include <memory>
#include <numeric>
#include <vector>
std::unique_ptr<std::vector<int>> make_range(int n) {
auto v = std::make_unique<std::vector<int>>();
for (int i = 1; i <= n; ++i) {
v->push_back(i);
}
return v; // ownership moves to the caller
}
int main() {
auto range = make_range(10);
std::cout << std::accumulate(range->begin(), range->end(), 0) << '\n';
}55This shows how ownership is returned. In real code, returning the std::vector<int> by value is simpler and just as fast.
Key takeaway#
Finished this lesson?