Shared Ownership with std::shared_ptr
Helper class -- a document that logs its lifetime
class Document {
std::string title_;
public:
explicit Document(std::string title) : title_(std::move(title)) {
std::cout << std::format("Document '{}' created\n", title_);
}
~Document() {
std::cout << std::format("Document '{}' destroyed\n", title_);
}
void print() const {
std::cout << std::format("Document: {}\n", title_);
}
const std::string& title() const { return title_; }
};Watch out: creating two separate shared_ptrs from the same raw pointer causes double-delete undefined behaviour. Always use make_shared or copy an existing shared_ptr: auto p = new Widget; std::shared_ptr<Widget> a(p); std::shared_ptr<Widget> b(p); // BUG: double-delete ----------------------------------------------- ----------------------------------------------- HOW IT WORKS: HOW MAKE_SHARED OPTIMIZES ALLOCATION ----------------------------------------------- std::make_shared<T>(args...) performs ONE heap allocation for both the managed object and the control block. They are placed in contiguous memory (the control block is typically placed right before or after the object).
Watch out: with make_shared, the object's memory cannot be freed until the LAST weak_ptr is also destroyed, because the object and control block share the same allocation -- you cannot free half of it. With separate allocations (shared_ptr<T>(new T)), the object's memory CAN be freed as soon as the strong count reaches 0, while the control block persists until the weak count also reaches 0. This matters if T is large and weak_ptrs are long-lived. -----------------------------------------------
void demonstrate_shared_ownership() {
std::cout << "--- Shared Ownership ---\n";
std::shared_ptr<Document> doc1;
{
// Create shared document
auto doc2 = std::make_shared<Document>("Report");
std::cout << std::format("Reference count: {}\n", doc2.use_count());
// Share ownership
doc1 = doc2;
std::cout << std::format("Reference count: {}\n", doc2.use_count());
// Store in container (another owner)
std::vector<std::shared_ptr<Document>> docs;
docs.push_back(doc2);
std::cout << std::format("Reference count: {}\n", doc2.use_count());
// doc2 and vector go out of scope here
}
std::cout << std::format("After scope - count: {}\n", doc1.use_count());
doc1->print(); // Still valid!
doc1.reset(); // Explicitly release
std::cout << "Document now destroyed\n";
}2. Circular references and std::weak_ptr
leaks -- the reference count never reaches zero. Use std::weak_ptr to break cycles. A weak_ptr observes the resource without contributing to the reference count.
Watch out: circular references between shared_ptrs cause memory
HOW WEAK_PTR AND LOCK() WORK Deep Dive
-- BAD: circular reference (would leak if both used shared_ptr) -- We demonstrate the fix directly: one direction uses weak_ptr.
struct Employee; // forward declaration
struct Team {
std::string name;
std::vector<std::shared_ptr<Employee>> members; // Team owns Employees
explicit Team(std::string n) : name(std::move(n)) {
std::cout << std::format("Team '{}' created\n", name);
}
~Team() {
std::cout << std::format("Team '{}' destroyed\n", name);
}
};
struct Employee {
std::string name;
std::weak_ptr<Team> team; // weak_ptr breaks the cycle!
explicit Employee(std::string n) : name(std::move(n)) {
std::cout << std::format("Employee '{}' created\n", name);
}
~Employee() {
std::cout << std::format("Employee '{}' destroyed\n", name);
}
void show_team() const {
// weak_ptr must be locked (promoted to shared_ptr) before use
if (auto t = team.lock()) {
std::cout << std::format("{} belongs to team '{}'\n",
name, t->name);
} else {
std::cout << std::format("{}'s team has been disbanded\n", name);
}
}
};
void demonstrate_weak_ptr() {
std::cout << "\n--- weak_ptr breaks circular references ---\n";
{
auto team = std::make_shared<Team>("Engineering");
auto alice = std::make_shared<Employee>("Alice");
auto bob = std::make_shared<Employee>("Bob");
// Team -> Employees (shared_ptr)
team->members.push_back(alice);
team->members.push_back(bob);
// Employees -> Team (weak_ptr -- does NOT increase ref count)
alice->team = team;
bob->team = team;
std::cout << std::format("Team ref count: {}\n", team.use_count());
// Count is 1 (only the local `team` variable), because
// weak_ptrs do not contribute to the count.
alice->show_team();
bob->show_team();
// Everything is destroyed cleanly when the scope ends.
}
std::cout << "All resources freed -- no leaks!\n";
}int main() {
// 1. Basic shared ownership
demonstrate_shared_ownership();
// 2. weak_ptr and circular reference prevention
demonstrate_weak_ptr();
return 0;
}