Docs
README
Smart Pointers in C++
Table of Contents
Why Smart Pointers
The Problem with Raw Pointers
void riskyFunction() {
int* data = new int[1000];
// What if processData() throws?
processData(data);
// What if we return early?
if (someCondition) {
return; // MEMORY LEAK!
}
delete[] data; // Might never reach here
}
The Solution: RAII with Smart Pointers
Smart pointers automatically manage memory through RAII:
- •Acquire resource in constructor
- •Release resource in destructor
- •No manual delete needed
#include <memory>
void safeFunction() {
auto data = make_unique<int[]>(1000);
processData(data.get()); // Even if throws, destructor runs
if (someCondition) {
return; // No leak! unique_ptr destructor frees memory
}
} // Automatically deleted here
Three Smart Pointer Types
| Type | Ownership | Use Case |
|---|---|---|
unique_ptr | Exclusive | Single owner, most common |
shared_ptr | Shared | Multiple owners |
weak_ptr | Non-owning | Breaking cycles, caching |
unique_ptr
The most common and efficient smart pointer.
Basic Usage
#include <memory>
// Create unique_ptr
unique_ptr<int> p1 = make_unique<int>(42); // Preferred
unique_ptr<int> p2(new int(42)); // Direct construction
// Access the value
cout << *p1 << endl; // Dereference
cout << p1.get() << endl; // Get raw pointer
// Reset to new value
p1.reset(new int(100)); // Delete old, point to new
p1.reset(); // Delete and become nullptr
// Check if pointing to something
if (p1) {
cout << "p1 is valid" << endl;
}
Arrays with unique_ptr
// Array specialization
unique_ptr<int[]> arr = make_unique<int[]>(10);
arr[0] = 42; // Bracket access
// Automatically calls delete[]
Move Semantics (No Copying!)
unique_ptr<int> p1 = make_unique<int>(42);
// CANNOT copy
// unique_ptr<int> p2 = p1; // ERROR!
// CAN move
unique_ptr<int> p2 = move(p1); // p1 is now nullptr
Common Operations
unique_ptr<int> ptr = make_unique<int>(42);
ptr.get(); // Raw pointer (don't delete!)
*ptr; // Dereference
ptr.reset(); // Delete and set to nullptr
ptr.reset(new_p); // Delete and point to new
ptr.release(); // Release ownership, return raw ptr
ptr = nullptr; // Same as reset()
if (ptr) { } // Check if valid
Transfer Ownership
unique_ptr<Widget> createWidget() {
return make_unique<Widget>(); // Move on return
}
void takeOwnership(unique_ptr<Widget> w) {
// Widget deleted when function ends
}
unique_ptr<Widget> w = createWidget();
takeOwnership(move(w)); // w is now nullptr
shared_ptr
For shared ownership - multiple pointers can own the same resource.
Basic Usage
#include <memory>
// Create shared_ptr
shared_ptr<int> sp1 = make_shared<int>(42); // Preferred
shared_ptr<int> sp2(new int(42)); // Direct
// Copy is allowed! (increases reference count)
shared_ptr<int> sp3 = sp1;
shared_ptr<int> sp4 = sp1;
cout << sp1.use_count() << endl; // 3 (sp1, sp3, sp4)
Reference Counting
{
shared_ptr<int> p1 = make_shared<int>(42); // count = 1
cout << "Count: " << p1.use_count() << endl;
{
shared_ptr<int> p2 = p1; // count = 2
shared_ptr<int> p3 = p1; // count = 3
cout << "Count: " << p1.use_count() << endl;
} // p2, p3 destroyed, count = 1
cout << "Count: " << p1.use_count() << endl;
} // p1 destroyed, count = 0, memory freed
Common Operations
shared_ptr<int> ptr = make_shared<int>(42);
ptr.get(); // Raw pointer
*ptr; // Dereference
ptr.use_count(); // Number of owners
ptr.unique(); // true if use_count() == 1
ptr.reset(); // Decrease count, maybe delete
ptr = nullptr; // Same as reset()
if (ptr) { } // Check if valid
Arrays with shared_ptr (C++17+)
// C++17 and later
shared_ptr<int[]> arr = make_shared<int[]>(10);
arr[0] = 42;
// Pre-C++17: use custom deleter
shared_ptr<int> arr(new int[10], default_delete<int[]>());
make_shared vs new
// Preferred: make_shared
auto p1 = make_shared<Widget>(args);
// Avoid: new
shared_ptr<Widget> p2(new Widget(args));
Why prefer make_shared:
- •One memory allocation (instead of two)
- •Exception safe
- •More efficient
weak_ptr
Non-owning observer of shared_ptr. Doesn't affect reference count.
Purpose
- •Break circular references
- •Caching - observe without keeping alive
- •Observer pattern - safely check if object still exists
Basic Usage
shared_ptr<int> sp = make_shared<int>(42);
weak_ptr<int> wp = sp; // Doesn't increase count
cout << sp.use_count() << endl; // Still 1
// Must convert to shared_ptr to use
if (auto locked = wp.lock()) {
cout << *locked << endl; // Safe to use
} else {
cout << "Object destroyed" << endl;
}
Detecting Expiration
shared_ptr<int> sp = make_shared<int>(42);
weak_ptr<int> wp = sp;
cout << wp.expired() << endl; // false
sp.reset(); // Object destroyed
cout << wp.expired() << endl; // true
Breaking Circular References
Without weak_ptr (memory leak):
struct Node {
shared_ptr<Node> next; // Circular!
shared_ptr<Node> prev; // Memory leak
};
With weak_ptr (correct):
struct Node {
shared_ptr<Node> next;
weak_ptr<Node> prev; // Won't keep alive
};
Example: Observer Pattern
class Subject {
vector<weak_ptr<Observer>> observers;
public:
void notify() {
for (auto& wp : observers) {
if (auto sp = wp.lock()) {
sp->update();
}
}
}
};
Custom Deleters
Customize how resources are freed.
With unique_ptr
// Lambda deleter
auto deleter = [](int* p) {
cout << "Custom delete" << endl;
delete p;
};
unique_ptr<int, decltype(deleter)> ptr(new int(42), deleter);
// Function pointer deleter
void myDeleter(FILE* f) {
if (f) fclose(f);
}
unique_ptr<FILE, decltype(&myDeleter)> file(fopen("test.txt", "r"), myDeleter);
With shared_ptr
// Easier - deleter is type-erased
shared_ptr<int> ptr(new int(42), [](int* p) {
cout << "Custom delete" << endl;
delete p;
});
// File example
shared_ptr<FILE> file(fopen("test.txt", "r"), fclose);
Practical Examples
// Managing C resources
shared_ptr<SDL_Window> window(
SDL_CreateWindow(...),
SDL_DestroyWindow
);
// Array with shared_ptr (pre-C++17)
shared_ptr<int> arr(new int[10], default_delete<int[]>());
// Custom pool allocator
auto poolDeleter = [&pool](Widget* w) { pool.deallocate(w); };
unique_ptr<Widget, decltype(poolDeleter)> w(pool.allocate(), poolDeleter);
Best Practices
✅ Do
// 1. Use make_unique/make_shared
auto p1 = make_unique<Widget>();
auto p2 = make_shared<Widget>();
// 2. Pass by value to transfer ownership
void takeOwnership(unique_ptr<Widget> w);
// 3. Pass by reference to use without ownership
void useWidget(const Widget& w);
void useWidget(Widget* w); // If nullable
// 4. Return unique_ptr from factories
unique_ptr<Widget> createWidget() {
return make_unique<Widget>();
}
// 5. Use weak_ptr for breaking cycles
struct Node {
shared_ptr<Node> next;
weak_ptr<Node> prev;
};
// 6. Check weak_ptr before use
if (auto sp = wp.lock()) {
// safe to use sp
}
❌ Don't
// 1. Don't use raw new with smart pointers (exception safety)
shared_ptr<Widget> p(new Widget()); // OK but not ideal
auto p = make_shared<Widget>(); // Better
// 2. Don't call delete on smart pointer's raw pointer
Widget* raw = ptr.get();
delete raw; // WRONG! Double delete
// 3. Don't create multiple smart pointers from raw pointer
Widget* raw = new Widget();
shared_ptr<Widget> p1(raw);
shared_ptr<Widget> p2(raw); // WRONG! Double delete
// 4. Don't use shared_ptr when unique_ptr suffices
// unique_ptr is more efficient
// 5. Don't return reference to local smart pointer's content
Widget& createWidget() {
auto p = make_unique<Widget>();
return *p; // DANGLING!
}
Guidelines Summary
- •Prefer unique_ptr - most efficient, clearest ownership
- •Use shared_ptr only when truly needed
- •Use weak_ptr to break cycles
- •Always use make_unique/make_shared
- •Pass unique_ptr by value to transfer ownership
- •Pass by reference/pointer when not taking ownership
- •Return unique_ptr from factory functions
Comparison
| Feature | Raw Pointer | unique_ptr | shared_ptr |
|---|---|---|---|
| Ownership | None | Exclusive | Shared |
| Copyable | Yes | No | Yes |
| Movable | Yes | Yes | Yes |
| Overhead | None | None | Reference count |
| Thread-safe | No | No | Count is atomic |
| Use case | Non-owning | Single owner | Multiple owners |
Quick Reference
#include <memory>
// unique_ptr
unique_ptr<T> p = make_unique<T>(args);
unique_ptr<T[]> arr = make_unique<T[]>(size);
p.get(); // Raw pointer
p.reset(); // Delete and nullify
p.release(); // Release ownership
unique_ptr<T> p2 = move(p); // Transfer ownership
// shared_ptr
shared_ptr<T> p = make_shared<T>(args);
p.use_count(); // Reference count
p.unique(); // true if count == 1
shared_ptr<T> p2 = p; // Copy (increments count)
// weak_ptr
weak_ptr<T> wp = sp;
wp.expired(); // True if object gone
wp.lock(); // Get shared_ptr (or nullptr)
// Common patterns
auto p = make_unique<Widget>(); // Factory
takeOwnership(move(p)); // Transfer
useWidget(*p); // Use by ref
Compile & Run
g++ -std=c++17 -Wall -Wextra examples.cpp -o examples
./examples