Smart pointers, introduced in C++11, are class templates that manage the lifetime of dynamically allocated objects. They act as wrappers around raw pointers, reducing the need for manual delete calls and helping prevent memory leaks. The standard library provides three primary smart pointer types: std::unique_ptr, std::shared_ptr, and std::weak_ptr.
Exclusive Ownership with unique_ptr
A unique_ptr ensures that at any time, only one pointer manages the underlying memory. The resource is automatically destroyed when the unique_ptr leaves its scope. Copy operations are disabled; ownership can only be transferred via std::move.
Constructing a unique_ptr can be done in several ways. The most recommended approach is std::make_unique, as it avoids explicit new and potential leaks.
#include <memory>
#include <iostream>
struct Widget {
std::string label;
Widget(const std::string& lbl) : label(lbl) {
std::cout << "Widget constructed: " << label << '\n';
}
~Widget() {
std::cout << "Widget destroyed: " << label << '\n';
}
void rename(const std::string& new_label) {
label = new_label;
}
};
int main() {
// From a raw pointer (less safe if raw pointer is later misused)
Widget* raw = new Widget("Alpha");
std::unique_ptr<Widget> up1{raw};
raw = nullptr;
// Direct initialization with new
std::unique_ptr<Widget> up2{new Widget("Beta")};
// Preferred: make_unique
auto up3 = std::make_unique<Widget>("Gamma");
up3->rename("GammaPrime");
std::cout << "up3 points to: " << up3->label << '\n';
// Retrieve raw address
std::cout << "Address stored in up3: " << up3.get() << '\n';
// Dereferencing
if (up3) {
Widget& w = *up3;
std::cout << "Dereferenced label: " << w.label << '\n';
}
return 0;
}
Passing unique_ptr to Functions
Ownership transfer is explicit. When passing by value, the caller must invoke std::move. If the argument is a temporary, the move happens automatically. Passing by reference allows temporary use without transferring ownership; a const reference prevents the called function from resetting the pointer.
#include <memory>
#include <iostream>
using WidgetPtr = std::unique_ptr<Widget>;
void consume_by_value(WidgetPtr ptr) {
if (ptr)
std::cout << "Inside value receiver: " << ptr->label << '\n';
}
void inspect_by_ref(const WidgetPtr& ptr) {
if (ptr) {
std::cout << "Inside const ref: " << ptr->label << '\n';
// ptr->rename("Changed"); // OK if rename is const
// ptr.reset(); // Error: disallowed on const reference
}
}
WidgetPtr create_widget(const std::string& name) {
auto local = std::make_unique<Widget>(name);
return local; // moved automatically
}
int main() {
auto item = std::make_unique<Widget>("Original");
// Transfer ownership
consume_by_value(std::move(item));
// item is now null
auto another = std::make_unique<Widget>("Second");
inspect_by_ref(another);
// Chaining: return value directly used
consume_by_value(create_widget("Temporary"));
return 0;
}
Reference-counted Shared Ownership with shared_ptr
shared_ptr allows multiple pointers to share the same resource. An internal control block stores a use count. Copying a shared_ptr increments the count; destroying or resetting one decrements it. The managed object is deleted when the count drops to zero.
#include <memory>
#include <iostream>
struct Document {
int id;
Document(int idx) : id(idx) {
std::cout << "Document " << id << " created\n";
}
~Document() {
std::cout << "Document " << id << " deleted\n";
}
};
int main() {
auto master = std::make_shared<Document>(100);
std::cout << "After master, count = " << master.use_count() << '\n';
{
std::shared_ptr<Document> copyA = master;
std::cout << "After copyA, count = " << master.use_count() << '\n';
std::shared_ptr<Document> copyB = master;
std::cout << "After copyB, count = " << master.use_count() << '\n';
} // copyA and copyB destroyed, count decreases
std::cout << "After inner scope, count = " << master.use_count() << '\n';
master.reset();
std::cout << "After reset, count = " << master.use_count() << '\n';
return 0;
}
Passing shared_ptr to Functions
Passing by value increments the reference count for the duration of the function body. Passing by const reference avoids the count manipulation while still allowing inspection. Returning a shared_ptr by value enables fluent chaining.
#include <memory>
#include <iostream>
std::shared_ptr<Document> build_document(int id) {
return std::make_shared<Document>(id);
}
void process_by_value(std::shared_ptr<Document> doc) {
std::cout << "Processing doc " << doc->id
<< ", local count = " << doc.use_count() << '\n';
}
void describe_by_const_ref(const std::shared_ptr<Document>& doc) {
if (doc)
std::cout << "Document ID: " << doc->id << '\n';
}
int main() {
auto first = std::make_shared<Document>(1);
process_by_value(first);
std::cout << "After call, count = " << first.use_count() << '\n';
describe_by_const_ref(first);
// Chained usage
build_document(99)->id = 999;
process_by_value(build_document(55));
return 0;
}
Converting Between unique_ptr and shared_ptr
You cannot construct a unique_ptr from a shared_ptr. However, a unique_ptr willingly transfers ownership to a shared_ptr via std::move. Returning a unique_ptr from a factory function is a flexible pattern because callers can decide to hold it as either a unique_ptr or shared_ptr.
#include <memory>
#include <iostream>
struct Gadget {
std::string name;
Gadget(const std::string& n) : name(n) {
std::cout << "Gadget " << name << " created\n";
}
~Gadget() { std::cout << "Gadget " << name << " destroyed\n"; }
};
std::unique_ptr<Gadget> manufacture_gadget(const std::string& label) {
return std::make_unique<Gadget>(label);
}
int main() {
std::unique_ptr<Gadget> prototype = std::make_unique<Gadget>("Pro");
std::shared_ptr<Gadget> shared = std::move(prototype);
std::cout << "shared use count: " << shared.use_count() << '\n';
// prototype is now null
// Direct conversion from returned unique_ptr
std::shared_ptr<Gadget> fromFactory = manufacture_gadget("LGadget");
if (fromFactory)
std::cout << "Factory gadget: " << fromFactory->name << '\n';
return 0;
}
Weak References with weak_ptr
A weak_ptr observes an object managed by shared_ptr without contributing to the reference count. It cannot directly dereference or access members; instead, the observer must call lock() to attempt to obtain a temporary shared_ptr that is valid only if the object still exists. This mechanism is essential for breaking reference cycles, for example, in graph-like structures where nodes refer to eachother.
#include <memory>
#include <iostream>
struct Node {
std::shared_ptr<Node> partner; // could create cycles if not broken
~Node() { std::cout << "Node freed\n"; }
};
int main() {
auto nodeA = std::make_shared<Node>();
auto nodeB = std::make_shared<Node>();
// Creating a cycle: each points to the other
nodeA->partner = nodeB;
nodeB->partner = nodeA;
// Without weak_ptr, these nodes leak.
// Practical solution: change one partner to weak_ptr.
// weak_ptr demonstration
std::shared_ptr<int> sharedInt = std::make_shared<int>(42);
std::weak_ptr<int> weakInt = sharedInt;
std::cout << "shared count: " << sharedInt.use_count() << '\n'; // 1
std::cout << "weak count: " << weakInt.use_count() << '\n'; // 1
if (auto locked = weakInt.lock()) {
std::cout << "Value via lock: " << *locked << '\n';
}
sharedInt.reset();
// Now weakInt is expired
std::cout << "After reset, expired? " << std::boolalpha << weakInt.expired() << '\n';
if (auto locked = weakInt.lock()) {
// This block will not execute
} else {
std::cout << "Lock failed: object already destroyed\n";
}
return 0;
}
By combining unique_ptr for exclusive ownership, shared_ptr for shared ownership, and weak_ptr to safely observe shared resources without extending lifetimes, modern C++ programs can articulate clear and robust memory management policies.