Modern C++ Fundamentals: From Basics to Smart Pointers and Threads · Ownership, Templates, Errors and Concurrency
Smart Pointers and Ownership
Replace raw new and delete with std::unique_ptr and std::shared_ptr, break reference cycles with std::weak_ptr, and learn the ownership rules that prevent leaks.
What you'll build
A small organization chart. A manager owns their reports, each report points back to the manager without creating a cycle, and a tracing class shows exactly when every object is created and destroyed.
The idea
Every object needs one clear owner responsible for destroying it. Raw pointers (T*) don't say who owns what, so modern C++ expresses ownership in the type:
std::unique_ptr<T>: sole ownership. It cannot be copied, only moved. The object is destroyed when the pointer goes out of scope. This is the default choice; it costs nothing compared to a raw pointer. Create withstd::make_unique<T>(args...).std::shared_ptr<T>: shared ownership with a reference count; the object is destroyed when the last owner goes. Create withstd::make_shared<T>(args...). Use it sparingly, only when ownership is genuinely shared.std::weak_ptr<T>: a non-owning observer of ashared_ptr. Calllock()to get ashared_ptrif the object is still alive, or an empty one if it isn't. Use it to break cycles: two objects holdingshared_ptrs to each other never reach zero and leak.- Raw pointers and references are for non-owning access: "use this, but don't free it".
Rules of thumb: don't write new or delete in application code; return unique_ptr from factory functions; pass T& or T* to functions that merely use an object.
The code
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Trace {
std::string name;
explicit Trace(std::string n) : name{std::move(n)} { std::cout << " create " << name << '\n'; }
~Trace() { std::cout << " destroy " << name << '\n'; }
};
struct Employee {
Trace trace;
std::weak_ptr<Employee> manager; // non-owning link back (no cycle)
std::vector<std::shared_ptr<Employee>> reports; // owning links down
explicit Employee(std::string name) : trace{std::move(name)} {}
};
std::unique_ptr<Trace> make_trace(const std::string& name) { // a factory returns ownership
return std::make_unique<Trace>(name);
}
int main() {
std::cout << "unique_ptr:\n";
{
auto a = make_trace("report.pdf");
auto b = std::move(a); // ownership moves; a is now empty
std::cout << " a is " << (a ? "set" : "empty") << ", b is " << (b ? "set" : "empty") << '\n';
} // b's Trace is destroyed here
std::cout << "shared_ptr and weak_ptr:\n";
{
auto boss = std::make_shared<Employee>("Boss");
auto dev = std::make_shared<Employee>("Dev");
dev->manager = boss; // weak: does not keep boss alive
boss->reports.push_back(dev);
std::cout << " boss use_count = " << boss.use_count() << '\n'; // 1
std::cout << " dev use_count = " << dev.use_count() << '\n'; // 2: dev + boss->reports
if (auto m = dev->manager.lock()) {
std::cout << " dev's manager is " << m->trace.name << '\n';
}
} // both released: no cycle, so both destructors run
std::cout << "done\n";
}
Output
unique_ptr: create report.pdf a is empty, b is set destroy report.pdf shared_ptr and weak_ptr: create Boss create Dev boss use_count = 1 dev use_count = 2 dev's manager is Boss destroy Dev destroy Boss done
Run this on your computer — see “Get Started” for setup.
Change dev->manager to a shared_ptr<Employee> and the two objects keep each other alive forever: the "destroy" lines never appear.
Common mistakes
- Using
newanddeletedirectly. An early return or exception between them leaks memory. Usemake_unique. - Reaching for
shared_ptrby default. It adds overhead and blurs ownership. Start withunique_ptr. - Creating
shared_ptrcycles. Parent-to-child owns; child-to-parent observes (weak_ptror a raw pointer). - Making two
shared_ptrs from the same raw pointer. Each has its own count, so the object is deleted twice. Create one withmake_sharedand copy it. - Passing smart pointers to functions that only use the object. Pass
T&orconst T&instead; accept a smart pointer only when taking or sharing ownership.
Key points
- Express ownership in types:
unique_ptrfor sole ownership,shared_ptrfor shared,weak_ptrto observe. - Create with
make_uniqueandmake_shared; avoidnewanddelete. - Break ownership cycles with
weak_ptr(or a non-owning raw pointer). - A raw
T*orT&means "borrow, don't own".
Further reading: cppreference: Dynamic memory management.
Practice questions
When would you choose `unique_ptr` over `shared_ptr`?
Answer
Almost always. unique_ptr models sole ownership with no overhead and a clear lifetime. Use shared_ptr only when several owners genuinely need to keep an object alive and none can be designated the single owner.
How does `weak_ptr` solve circular references?
Answer
A weak_ptr observes an object managed by shared_ptr without increasing the reference count. If a child refers to its parent through a weak_ptr, the parent's count can reach zero, so both objects are destroyed.
Open in the app to mark lessons complete, take notes and track your progress.