Smart pointers in C++ are wrapper objects that behave like raw pointers but automatically manage the lifetime of the object they point to. Introduced in C++11, std::unique_ptr, std::shared_ptr, and std::weak_ptr eliminate the need for manual delete calls, prevent memory leaks, and make ownership semantics explicit and safe — all with little to no runtime overhead compared to raw pointers.
Introduction
For decades, C++ programmers managed heap memory manually using new and delete. While this approach gives maximum control, it also creates a class of bugs that are notoriously difficult to track down: memory leaks (forgetting to delete), dangling pointers (using memory after it has been freed), double frees (calling delete twice on the same pointer), and use-after-free errors. These bugs are responsible for a staggering proportion of security vulnerabilities and production crashes in C++ software.
C++11 introduced a family of smart pointers in the <memory> header that solve these problems definitively. Rather than replacing the concept of a pointer, smart pointers wrap a raw pointer in a class that enforces clear ownership rules and calls delete automatically at the right time. The three primary smart pointers — std::unique_ptr, std::shared_ptr, and std::weak_ptr — each represent a different ownership model.
This article teaches you each of these three smart pointers from the ground up. You will understand what problem each one solves, how to construct and use them, when to prefer one over another, and how to avoid the common mistakes that trip up developers new to smart pointers. Every concept is grounded in practical, runnable code with thorough step-by-step explanations.
The Problem with Raw Pointers
Before diving into smart pointers, it is worth seeing concretely what problems they are designed to solve.
#include <iostream>
using namespace std;
class Resource {
public:
Resource(int id) : id_(id) {
cout << "Resource " << id_ << " acquired" << endl;
}
~Resource() {
cout << "Resource " << id_ << " released" << endl;
}
void use() {
cout << "Using Resource " << id_ << endl;
}
private:
int id_;
};
void riskyFunction(bool throwError) {
Resource* res = new Resource(1); // Heap allocation
res->use();
if (throwError) {
throw runtime_error("Something went wrong!");
// 'res' is never deleted — MEMORY LEAK
}
delete res; // Only reached if no exception
cout << "Resource properly cleaned up" << endl;
}
int main() {
// Case 1: No error — works fine
try {
riskyFunction(false);
} catch (...) {}
cout << "---" << endl;
// Case 2: Exception thrown — resource leaks!
try {
riskyFunction(true);
} catch (const exception& e) {
cout << "Caught: " << e.what() << endl;
}
return 0;
}Output:
Resource 1 acquired
Using Resource 1
Resource 1 released
Resource properly cleaned up
---
Resource 1 acquired
Using Resource 1
Caught: Something went wrong!Step-by-step explanation:
- In
riskyFunction(false), everything works fine:Resourceis allocated, used, and deleted. The destructor message confirms cleanup. - In
riskyFunction(true), an exception is thrown beforedelete resis reached. The function exits via the exception path, skipping thedeletecall entirely. Notice that “Resource 1 released” is never printed — the resource has leaked. - This is the fundamental fragility of raw pointer memory management. Every code path — including exception paths — must lead to
delete. In real code with multiple return points, error conditions, and deeply nested logic, guaranteeing this is extremely difficult. - Smart pointers solve this by tying the lifetime of the heap object to the lifetime of the smart pointer wrapper. When the smart pointer goes out of scope — for any reason, including exceptions — the wrapped object is automatically destroyed. This is the RAII principle in action.
std::unique_ptr: Exclusive Ownership
std::unique_ptr<T> represents exclusive ownership of a heap-allocated object. There is exactly one unique_ptr that owns the resource at any given time. When that unique_ptr is destroyed (goes out of scope or is explicitly reset), it automatically deletes the owned object.
The “unique” in unique_ptr means it cannot be copied — only moved. This guarantees at the language level that there is never more than one owner.
Creating and Using unique_ptr
#include <iostream>
#include <memory>
using namespace std;
class Resource {
public:
Resource(int id) : id_(id) {
cout << "Resource " << id_ << " acquired" << endl;
}
~Resource() {
cout << "Resource " << id_ << " released" << endl;
}
void use() {
cout << "Using Resource " << id_ << endl;
}
private:
int id_;
};
int main() {
// Create a unique_ptr using make_unique (preferred)
unique_ptr<Resource> ptr = make_unique<Resource>(42);
// Use it just like a raw pointer
ptr->use();
(*ptr).use(); // Dereference operator also works
cout << "ptr is " << (ptr ? "not null" : "null") << endl;
// No need to call delete — happens automatically at end of scope
cout << "Leaving scope..." << endl;
return 0; // ptr goes out of scope here, destructor is called
}Output:
Resource 42 acquired
Using Resource 42
Using Resource 42
ptr is not null
Leaving scope...
Resource 42 releasedStep-by-step explanation:
make_unique<Resource>(42)creates aResourceobject on the heap and wraps it in aunique_ptr. The argument42is forwarded toResource‘s constructor. Usingmake_uniqueis always preferred overnewbecause it is exception-safe and prevents raw pointers from ever being exposed.- The
->operator onunique_ptrbehaves exactly like the->operator on a raw pointer, forwarding method calls to the underlying object. The dereference operator*ptralso works as expected. - The boolean conversion of
unique_ptr(used inptr ? "not null" : "null") returnstrueif the pointer is non-null, exactly like a raw pointer. - When
main()returns,ptrgoes out of scope. Its destructor is called automatically, which callsdeleteon the managedResource. The destructor message confirms cleanup — no manualdeleteneeded anywhere.
unique_ptr and Exception Safety
#include <iostream>
#include <memory>
using namespace std;
class Resource {
public:
Resource(int id) : id_(id) {
cout << "Resource " << id_ << " acquired" << endl;
}
~Resource() {
cout << "Resource " << id_ << " released" << endl;
}
void use() { cout << "Using Resource " << id_ << endl; }
private:
int id_;
};
void safeFunction(bool throwError) {
// unique_ptr guarantees cleanup even if an exception occurs
auto res = make_unique<Resource>(1);
res->use();
if (throwError) {
throw runtime_error("Something went wrong!");
// unique_ptr destructor STILL runs — no leak!
}
cout << "No error — normal exit" << endl;
} // res destroyed here (or during stack unwinding)
int main() {
cout << "--- No error ---" << endl;
safeFunction(false);
cout << "--- With error ---" << endl;
try {
safeFunction(true);
} catch (const exception& e) {
cout << "Caught: " << e.what() << endl;
}
return 0;
}Output:
--- No error ---
Resource 1 acquired
Using Resource 1
No error — normal exit
Resource 1 released
--- With error ---
Resource 1 acquired
Using Resource 1
Resource 1 released
Caught: Something went wrong!Step-by-step explanation:
- In the error case,
unique_ptr‘s destructor is called during stack unwinding — the process by which C++ cleans up local variables when an exception propagates. Even though the exception bypasses normal control flow, all local objects (including smart pointers) on the stack are properly destroyed. - “Resource 1 released” now appears even when an exception is thrown. The leak that existed with raw pointers is completely eliminated.
- This is the core value proposition of
unique_ptr: automatic, reliable cleanup regardless of how a function exits.
Transferring Ownership with std::move
Because unique_ptr cannot be copied, ownership is transferred using std::move, which converts an lvalue into an rvalue reference, allowing the move constructor or move assignment operator to transfer ownership.
#include <iostream>
#include <memory>
using namespace std;
class Widget {
public:
Widget(string name) : name_(name) {
cout << "Widget '" << name_ << "' created" << endl;
}
~Widget() {
cout << "Widget '" << name_ << "' destroyed" << endl;
}
void show() const {
cout << "Widget: " << name_ << endl;
}
private:
string name_;
};
unique_ptr<Widget> createWidget(string name) {
// Factory function: ownership transferred to caller
return make_unique<Widget>(name);
}
void takeOwnership(unique_ptr<Widget> w) {
// This function now owns the widget
w->show();
} // Widget destroyed here
int main() {
// Ownership transferred from factory to 'w1'
auto w1 = createWidget("Button");
w1->show();
// Transfer ownership from w1 to w2 using move
auto w2 = move(w1);
cout << "w1 is " << (w1 ? "valid" : "null") << endl; // w1 is now null
cout << "w2 is " << (w2 ? "valid" : "null") << endl; // w2 holds the widget
w2->show();
// Transfer ownership into a function
takeOwnership(move(w2));
cout << "w2 is now " << (w2 ? "valid" : "null") << endl; // null
return 0;
}Output:
Widget 'Button' created
Widget: Button
w1 is null
w2 is valid
Widget: Button
Widget: Button
Widget 'Button' destroyed
w2 is now nullStep-by-step explanation:
createWidgetreturns aunique_ptr<Widget>by value. Return Value Optimization (RVO) or move semantics transfer ownership from the local variable inside the function tow1inmain()without any copying.auto w2 = move(w1)moves ownership fromw1tow2. After this line,w1holdsnullptr— it no longer owns the widget.w2is the new sole owner. This enforces the “unique” invariant at the language level.- Attempting to copy
w1intow2directly (withoutmove) would result in a compile error becauseunique_ptr‘s copy constructor is explicitly deleted. This is intentional — the compiler prevents accidental ownership sharing. takeOwnership(move(w2))transfers ownership into the function parameter. When the function returns, the parameter goes out of scope and theWidgetis destroyed.
unique_ptr for Arrays
unique_ptr also supports managing heap-allocated arrays with a specialized template:
// Allocate array of 5 integers
auto arr = make_unique<int[]>(5);
for (int i = 0; i < 5; i++) {
arr[i] = i * 10;
}
for (int i = 0; i < 5; i++) {
cout << arr[i] << " ";
}
// Output: 0 10 20 30 40
// Array deleted automatically — calls delete[]The unique_ptr<T[]> specialization calls delete[] instead of delete when it is destroyed, which is the correct way to free an array.
std::shared_ptr: Shared Ownership
std::shared_ptr<T> represents shared ownership of a heap-allocated object. Multiple shared_ptr instances can point to the same object simultaneously. The object is destroyed only when the last shared_ptr owning it is destroyed or reset. This is tracked by a reference count — an integer that is incremented every time a new shared_ptr is created from the same object, and decremented every time one is destroyed.
Creating and Using shared_ptr
#include <iostream>
#include <memory>
using namespace std;
class Resource {
public:
Resource(string name) : name_(name) {
cout << "Resource '" << name_ << "' acquired" << endl;
}
~Resource() {
cout << "Resource '" << name_ << "' released" << endl;
}
void use() const {
cout << "Using Resource '" << name_ << "'" << endl;
}
private:
string name_;
};
int main() {
cout << "--- Creating shared ownership ---" << endl;
shared_ptr<Resource> ptr1 = make_shared<Resource>("DatabaseConnection");
cout << "Reference count: " << ptr1.use_count() << endl; // 1
{
shared_ptr<Resource> ptr2 = ptr1; // Copying is allowed!
cout << "Reference count after copy: " << ptr1.use_count() << endl; // 2
shared_ptr<Resource> ptr3 = ptr1;
cout << "Reference count after second copy: " << ptr1.use_count() << endl; // 3
ptr2->use();
ptr3->use();
cout << "Leaving inner scope..." << endl;
} // ptr2 and ptr3 destroyed here — count drops to 1
cout << "Reference count after inner scope: " << ptr1.use_count() << endl; // 1
cout << "Leaving main..." << endl;
return 0; // ptr1 destroyed — count drops to 0, Resource released
}Output:
--- Creating shared ownership ---
Resource 'DatabaseConnection' acquired
Reference count: 1
Reference count after copy: 2
Reference count after second copy: 3
Using Resource 'DatabaseConnection'
Using Resource 'DatabaseConnection'
Leaving inner scope...
Reference count after inner scope: 1
Leaving main...
Resource 'DatabaseConnection' releasedStep-by-step explanation:
make_shared<Resource>("DatabaseConnection")creates theResourceon the heap and also allocates the control block — a small heap allocation that contains the reference count, weak reference count, and deleter. Usingmake_sharedcombines the object and control block into a single allocation, which is more efficient than usingnewdirectly.ptr1.use_count()returns the current reference count. After creation, it is 1 (onlyptr1owns the resource).shared_ptr<Resource> ptr2 = ptr1copies theshared_ptr. Unlikeunique_ptr, copying is fully allowed. The copy increments the reference count to 2. Bothptr1andptr2now co-own the sameResourceobject.- When
ptr2andptr3go out of scope at the end of the inner block, their destructors decrement the reference count. After the block, the count is back to 1 — onlyptr1still owns the resource. - When
main()returns andptr1is destroyed, the count drops to 0. This triggers the deletion of both theResourceobject and the control block. The destructor message confirms cleanup.
The Control Block
Every shared_ptr that shares ownership of an object points to the same control block — a separate heap allocation that contains:
- Strong reference count: how many
shared_ptrinstances own the object. - Weak reference count: how many
weak_ptrinstances observe the object (explained later). - Deleter: a function that knows how to destroy the managed object.
- Allocator: information for managing the control block’s own memory.
When the strong reference count reaches zero, the managed object is destroyed. When both the strong and weak reference counts reach zero, the control block itself is freed.
Shared Ownership in Practice: Caching and Shared Resources
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Texture {
public:
string filename;
int width, height;
Texture(string f, int w, int h) : filename(f), width(w), height(h) {
cout << "Loading texture: " << filename << " (" << w << "x" << h << ")" << endl;
}
~Texture() {
cout << "Unloading texture: " << filename << endl;
}
};
class Sprite {
public:
string name;
shared_ptr<Texture> texture; // Sprites share textures
Sprite(string n, shared_ptr<Texture> tex)
: name(n), texture(tex) {}
void draw() const {
cout << "Drawing sprite '" << name << "' using texture '"
<< texture->filename << "'" << endl;
}
};
int main() {
// Load texture once
auto grassTex = make_shared<Texture>("grass.png", 512, 512);
cout << "Texture ref count: " << grassTex.use_count() << endl; // 1
// Many sprites share the same texture
vector<Sprite> sprites;
sprites.emplace_back("GrassTile_1", grassTex);
sprites.emplace_back("GrassTile_2", grassTex);
sprites.emplace_back("GrassTile_3", grassTex);
cout << "Texture ref count after 3 sprites: " << grassTex.use_count() << endl; // 4
for (const auto& s : sprites) {
s.draw();
}
cout << "Clearing sprites vector..." << endl;
sprites.clear(); // All sprites destroyed — count drops back to 1
cout << "Texture ref count: " << grassTex.use_count() << endl; // 1
cout << "Texture still alive in main." << endl;
return 0; // grassTex destroyed — texture unloaded
}Output:
Loading texture: grass.png (512x512)
Texture ref count: 1
Texture ref count after 3 sprites: 4
Drawing sprite 'GrassTile_1' using texture 'grass.png'
Drawing sprite 'GrassTile_2' using texture 'grass.png'
Drawing sprite 'GrassTile_3' using texture 'grass.png'
Clearing sprites vector...
Texture ref count: 1
Texture still alive in main.
Unloading texture: grass.pngStep-by-step explanation:
- A single
Textureobject is loaded once and wrapped in ashared_ptr. MultipleSpriteobjects each store a copy of thisshared_ptr, sharing ownership of the texture. The texture is loaded only once, no matter how many sprites use it. - When the sprites vector is cleared, each
Spritedestructor decrements the reference count. But becausegrassTexinmain()still holds a reference, the count goes from 4 to 1 — the texture is not freed yet. - The texture is only unloaded when
grassTexitself is destroyed at the end ofmain(). This is the classic shared ownership pattern: the resource lives as long as anyone needs it.
std::weak_ptr: Non-Owning Observation
std::weak_ptr<T> is a companion to shared_ptr that holds a non-owning reference to an object managed by shared_ptr. A weak_ptr does not contribute to the reference count, so the object can be destroyed even while weak_ptr instances exist. Before using a weak_ptr, you must convert it to a shared_ptr (using lock()) to safely access the object — and this conversion fails gracefully if the object has already been destroyed.
weak_ptr solves two specific problems: breaking cyclic references and safe optional observation.
The Cyclic Reference Problem
When two objects hold shared_ptr to each other, neither ever reaches a reference count of zero — even when no external code holds any references to either object. This is a reference cycle, and it causes memory leaks even with shared_ptr.
#include <iostream>
#include <memory>
using namespace std;
struct Node {
string name;
shared_ptr<Node> next; // Problem: creates a reference cycle
Node(string n) : name(n) {
cout << "Node '" << name << "' created" << endl;
}
~Node() {
cout << "Node '" << name << "' destroyed" << endl;
}
};
int main() {
cout << "--- Cyclic reference (LEAK) ---" << endl;
{
auto a = make_shared<Node>("A");
auto b = make_shared<Node>("B");
a->next = b; // A owns B
b->next = a; // B owns A — CYCLE!
cout << "a use_count: " << a.use_count() << endl; // 2
cout << "b use_count: " << b.use_count() << endl; // 2
}
// a and b go out of scope:
// a's count: 2 -> 1 (b still holds a reference to a)
// b's count: 2 -> 1 (a still holds a reference to b)
// Neither reaches 0 — both nodes leak!
cout << "End of scope — were nodes destroyed?" << endl;
return 0;
}Output:
--- Cyclic reference (LEAK) ---
Node 'A' created
Node 'B' created
a use_count: 2
b use_count: 2
End of scope — were nodes destroyed?Notice: Neither ~Node() destructor message ever appears. Both nodes have leaked.
Breaking the Cycle with weak_ptr
#include <iostream>
#include <memory>
using namespace std;
struct Node {
string name;
weak_ptr<Node> next; // Non-owning: doesn't keep nodes alive
Node(string n) : name(n) {
cout << "Node '" << name << "' created" << endl;
}
~Node() {
cout << "Node '" << name << "' destroyed" << endl;
}
void visit() {
// Must lock() weak_ptr before use
if (auto nextNode = next.lock()) {
cout << name << " -> " << nextNode->name << endl;
} else {
cout << name << " -> (no next node)" << endl;
}
}
};
int main() {
cout << "--- With weak_ptr (no leak) ---" << endl;
{
auto a = make_shared<Node>("A");
auto b = make_shared<Node>("B");
a->next = b; // weak_ptr assignment — no reference count increase
b->next = a; // weak_ptr assignment — no reference count increase
cout << "a use_count: " << a.use_count() << endl; // 1
cout << "b use_count: " << b.use_count() << endl; // 1
a->visit();
b->visit();
}
// a's count: 1 -> 0 — destroyed!
// b's count: 1 -> 0 — destroyed!
cout << "End of scope" << endl;
return 0;
}Output:
--- With weak_ptr (no leak) ---
Node 'A' created
Node 'B' created
a use_count: 1
b use_count: 1
A -> B
B -> A
Node 'A' destroyed
Node 'B' destroyed
End of scopeStep-by-step explanation:
- By changing
shared_ptr<Node> nexttoweak_ptr<Node> next, thenextpointers no longer contribute to the reference count. Each node’s strong reference count stays at 1 (owned only by the local variable inmain()). - When the scope ends,
a‘s count drops from 1 to 0 andNode Ais destroyed. Thenb‘s count drops from 1 to 0 andNode Bis destroyed. Both destructor messages confirm clean cleanup. - To use a
weak_ptr, you call.lock(), which returns ashared_ptr. If the pointed-to object still exists,lock()returns a validshared_ptrwith an incremented reference count, keeping the object alive for the duration of that temporaryshared_ptr. If the object has already been destroyed,lock()returns an empty (null)shared_ptr. - The
if (auto nextNode = next.lock())pattern is the idiomatic way to safely use aweak_ptr. It checks both that the object exists and acquires a temporary owning handle in a single expression.
weak_ptr for Cache/Observer Patterns
#include <iostream>
#include <memory>
#include <vector>
#include <algorithm>
using namespace std;
class EventSystem {
public:
// Store observers as weak_ptr — doesn't prevent their destruction
vector<weak_ptr<class Observer>> observers;
void registerObserver(shared_ptr<class Observer> obs) {
observers.push_back(obs);
}
void notify(const string& event); // Defined after Observer
};
class Observer {
public:
string name;
Observer(string n) : name(n) {
cout << "Observer '" << name << "' created" << endl;
}
~Observer() {
cout << "Observer '" << name << "' destroyed" << endl;
}
void onEvent(const string& event) {
cout << "[" << name << "] Received event: " << event << endl;
}
};
void EventSystem::notify(const string& event) {
// Remove expired weak_ptrs and notify valid ones
auto it = observers.begin();
while (it != observers.end()) {
if (auto obs = it->lock()) {
obs->onEvent(event);
++it;
} else {
cout << "Removing expired observer" << endl;
it = observers.erase(it);
}
}
}
int main() {
EventSystem events;
auto obs1 = make_shared<Observer>("Logger");
auto obs2 = make_shared<Observer>("Dashboard");
events.registerObserver(obs1);
events.registerObserver(obs2);
events.notify("UserLogin");
cout << "--- obs2 goes out of scope ---" << endl;
obs2.reset(); // Destroy obs2
events.notify("UserLogout");
return 0;
}Output:
Observer 'Logger' created
Observer 'Dashboard' created
[Logger] Received event: UserLogin
[Dashboard] Received event: UserLogin
--- obs2 goes out of scope ---
Observer 'Dashboard' destroyed
[Logger] Received event: UserLogout
Removing expired observerStep-by-step explanation:
EventSystemstoresweak_ptr<Observer>instead ofshared_ptr<Observer>. This means the event system does not prevent observers from being destroyed when their owners let go of them.- On the first
notify("UserLogin"), both observers are alive.lock()succeeds for both, and both receive the event. - After
obs2.reset(),Observer 'Dashboard'is destroyed immediately (its reference count drops to 0). The event system’sweak_ptrto it is now expired. - On the second
notify("UserLogout"),lock()fails for the expiredweak_ptr. The code detects this, prints a message, and erases the expired entry. OnlyLoggerreceives the event. The event system gracefully handles the case where an observer has been destroyed without requiring any explicit unregistration step.
Comparing the Three Smart Pointers
| Feature | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| Ownership model | Exclusive (one owner) | Shared (multiple owners) | Non-owning observer |
| Copyable? | No (move-only) | Yes | Yes |
| Movable? | Yes | Yes | Yes |
| Reference count? | No | Yes (strong count) | Tracks weak count only |
| Overhead vs raw pointer | Minimal (zero-cost abstraction) | Control block + atomic ref count | Pointer + weak count check |
| Destroys object when? | When unique_ptr goes out of scope | When last shared_ptr is destroyed | Never (non-owning) |
| Access requires check? | No (use directly) | No (use directly) | Yes (must call lock()) |
| Solves cyclic references? | N/A | No (causes them) | Yes (breaks cycles) |
| Header | <memory> | <memory> | <memory> |
| Typical use case | Factory results, class members, containers | Shared resources, caches, event listeners | Observers, back-pointers, optional handles |
Choosing the Right Smart Pointer
The decision of which smart pointer to use comes down to ownership semantics:
Use unique_ptr when:
- A single owner is responsible for the lifetime of the object (the common case).
- The object is created in one place and passed to one consumer.
- You are writing a factory function that hands off ownership to the caller.
- Performance is critical —
unique_ptrhas essentially zero overhead compared to raw pointers.
Use shared_ptr when:
- Multiple parts of the code need to keep the object alive simultaneously.
- The lifetime of the object is determined collectively by multiple owners.
- You are implementing caches, resource managers, or event dispatch systems where objects are shared.
- The order of destruction is nondeterministic across multiple owners.
Use weak_ptr when:
- You need to observe or optionally access a
shared_ptr-managed object without extending its lifetime. - You are breaking a cyclic reference between
shared_ptrobjects. - You are implementing the observer pattern where observers should not keep the subject alive.
- You want a “safe optional raw pointer” — a handle that gracefully becomes null when the target is destroyed.
The default should be unique_ptr. If you reach for shared_ptr, ask yourself: is this truly shared ownership, or can the design be simplified to a single owner? Unnecessary shared_ptr usage wastes memory (the control block), incurs atomic reference count overhead, and makes ownership semantics harder to reason about.
Custom Deleters
Both unique_ptr and shared_ptr support custom deleters — functions called instead of the default delete when the managed object is destroyed. This is essential for managing resources that are not created with new, such as file handles, network sockets, or C library resources.
#include <iostream>
#include <memory>
#include <cstdio>
using namespace std;
// Custom deleter for FILE*
struct FileCloser {
void operator()(FILE* f) const {
if (f) {
cout << "Closing file" << endl;
fclose(f);
}
}
};
int main() {
// Manage a C-style FILE* with unique_ptr and custom deleter
unique_ptr<FILE, FileCloser> file(fopen("test.txt", "w"), FileCloser{});
if (file) {
fputs("Hello, smart pointer!", file.get());
cout << "Wrote to file" << endl;
}
// file automatically closed when unique_ptr goes out of scope
cout << "Leaving scope..." << endl;
return 0;
}Output:
Wrote to file
Leaving scope...
Closing fileStep-by-step explanation:
FILE*is a C-style resource that is opened withfopenand must be closed withfclose. It is not created withnew, so the defaultdeleteis wrong.unique_ptr<FILE, FileCloser>accepts a second template argument: the type of the custom deleter.FileCloseris a functor (a class with anoperator()) that callsfcloseinstead ofdelete.file.get()returns the rawFILE*pointer, needed for functions likefputsthat work with raw pointers.- When
filegoes out of scope,FileCloser::operator()is called, properly closing the file. - Lambda expressions are also commonly used as custom deleters:
unique_ptr<FILE, decltype(&fclose)> f(fopen(...), &fclose).
Creating shared_ptr from this: enable_shared_from_this
A tricky problem arises when an object needs to create a shared_ptr to itself from within a member function. Naively wrapping this in a shared_ptr creates a second, independent control block — causing a double-free when both shared_ptr instances are destroyed.
The solution is std::enable_shared_from_this<T>:
#include <iostream>
#include <memory>
using namespace std;
class Task : public enable_shared_from_this<Task> {
public:
string name;
Task(string n) : name(n) {
cout << "Task '" << name << "' created" << endl;
}
~Task() {
cout << "Task '" << name << "' destroyed" << endl;
}
// Returns a shared_ptr to this task — safe version
shared_ptr<Task> getSelf() {
return shared_from_this(); // Uses the existing control block
}
};
int main() {
auto task = make_shared<Task>("Render");
cout << "Count after creation: " << task.use_count() << endl; // 1
auto self = task->getSelf();
cout << "Count after getSelf(): " << task.use_count() << endl; // 2
// Both 'task' and 'self' point to the same object with a shared control block
cout << "Same object? " << (task.get() == self.get() ? "Yes" : "No") << endl;
return 0;
}Output:
Task 'Render' created
Count after creation: 1
Count after getSelf(): 2
Same object? Yes
Task 'Render' destroyedStep-by-step explanation:
Taskinherits fromenable_shared_from_this<Task>. This mixin adds aweak_ptrtothisinside the object, which is initialized when the firstshared_ptris created viamake_shared.shared_from_this()uses that storedweak_ptrto return ashared_ptrthat shares the existing control block. The reference count increments from 1 to 2 — no new control block is created.- Both
taskandselfpoint to the sameTaskobject, confirmed by comparing the raw pointers with.get(). - When both go out of scope, the count drops from 2 to 0 and the object is destroyed once. Without
enable_shared_from_this, callingshared_ptr<Task>(this)inside a member function would create a second control block, and the object would bedeleted twice — undefined behavior.
Common Mistakes to Avoid
Mistake 1: Constructing shared_ptr Directly from new
// BAD: Two separate heap allocations (object + control block)
shared_ptr<Widget> p(new Widget());
// GOOD: Single heap allocation for both
auto p = make_shared<Widget>();make_shared is more efficient because it allocates the object and control block in a single call. It also avoids a subtle exception-safety issue with new.
Mistake 2: Sharing Ownership of a Stack Object
int x = 42;
shared_ptr<int> p(&x); // BAD: shared_ptr will try to delete x, which is on the stack!Never point a smart pointer at a stack-allocated variable. Smart pointers are for heap-allocated memory only.
Mistake 3: Calling shared_from_this Before a shared_ptr Exists
class Foo : public enable_shared_from_this<Foo> {
public:
Foo() {
auto self = shared_from_this(); // THROWS: no shared_ptr exists yet
}
};shared_from_this() can only be called after the object has been wrapped in a shared_ptr. Calling it in the constructor causes a std::bad_weak_ptr exception because the weak_ptr inside the object has not been initialized yet.
Mistake 4: Mixing Raw Pointers and Smart Pointers
auto p = make_shared<Widget>();
Widget* raw = p.get();
shared_ptr<Widget> p2(raw); // BAD: Second control block, double free!Never create a shared_ptr from a raw pointer that is already managed by another shared_ptr. Use shared_from_this() if you need a second owning handle to the same object from within the object itself, or simply copy the existing shared_ptr.
Mistake 5: Storing Raw Pointers from smart pointers Long-Term
auto sp = make_shared<Widget>();
Widget* raw = sp.get();
sp.reset(); // shared_ptr destroyed — Widget deleted
raw->doSomething(); // DANGLING POINTER: undefined behaviorget() returns a raw pointer to the managed object. This raw pointer becomes invalid as soon as the smart pointer is destroyed. Only use get() to pass the pointer to APIs that need a raw pointer and don’t take ownership.
Performance Considerations
Smart pointers are designed to be as efficient as possible, but it helps to understand where overhead exists:
unique_ptr is a true zero-cost abstraction in most cases. In optimized builds, a unique_ptr<T> with a default deleter is the same size as a raw pointer (T*) and generates the same machine code for access and destruction. There is no runtime overhead.
shared_ptr carries measurable overhead. Each shared_ptr is typically two machine words in size (a pointer to the object and a pointer to the control block). Reference count operations use atomic increments and decrements to be thread-safe. These atomic operations are significantly more expensive than non-atomic arithmetic, especially on multicore systems where they can cause cache contention. For this reason, you should prefer unique_ptr when shared ownership is not genuinely required.
weak_ptr adds a small cost when converting to shared_ptr via lock(), which performs an atomic check-and-increment. The cost is similar to a shared_ptr copy.
Practical Design Guidelines
Following a few simple rules will help you use smart pointers correctly in real projects:
Express ownership in function signatures. If a function takes ownership, accept a unique_ptr by value. If a function shares ownership, accept a shared_ptr by value. If a function just uses the object without taking or sharing ownership, accept a raw pointer or reference (since raw pointers and references are not owning).
Return unique_ptr from factory functions. Factory functions that create objects on the heap should return unique_ptr. The caller can then either keep it as unique_ptr (exclusive ownership) or convert it to shared_ptr if shared ownership is needed later — a unique_ptr is implicitly convertible to shared_ptr.
unique_ptr<Widget> makeWidget() {
return make_unique<Widget>();
}
// Caller can choose ownership model:
auto uw = makeWidget(); // unique ownership
shared_ptr<Widget> sw = makeWidget(); // convert to shared ownershipAvoid shared_ptr in hot paths. If a loop or frequently-called function copies shared_ptr objects repeatedly, the atomic reference count operations can become a bottleneck. Use raw references or raw pointers (non-owning) in inner loops, holding the shared_ptr alive in the outer scope.
Document ownership with const. A const unique_ptr<T> prevents the pointer itself from being moved or reset, while still allowing mutation of the pointed-to object. A const shared_ptr<T> similarly prevents reassignment of the pointer.
Conclusion
Smart pointers are one of the most impactful features in modern C++. They transform memory management from a manual, error-prone chore into a declarative expression of ownership semantics, all while generating code that is as fast as hand-written raw pointer management.
std::unique_ptr is the right choice for the vast majority of heap allocations. It enforces exclusive ownership, transfers cleanly with std::move, and has zero runtime overhead. Use it as your default.
std::shared_ptr handles genuinely shared resources where multiple owners collaborate to keep an object alive. Its reference counting makes it slightly heavier than unique_ptr, so reach for it only when shared ownership is truly required.
std::weak_ptr is the essential companion to shared_ptr, enabling non-owning observation, breaking cyclic references, and implementing safe optional handles to shared objects.
Together, these three tools give you a complete, principled system for managing heap memory in C++ — one that eliminates entire categories of bugs while preserving the performance and control that makes C++ such a powerful language.








