C++ Smart Pointers
Introduction: A circular reference bug that took three days to find
In the previous post we thought we had fixed leaks with unique_ptr. But when the program ran for a long time, memory still grew.
Valgrind still reported no definite leak: Memcheck only calls a block lost when nothing reachable points to it, and memory hanging off a live owner shows up merely as “still reachable”. (See Valgrind and leaks & ASan in practice.) After three days of digging, we found a circular reference (A holds B and B holds A, so neither side can be destroyed).
Memory context: unique_ptr and shared_ptr work most safely when tied to scope and lifetime with RAII; ownership transfer pairs well with move semantics. Rust ownership and borrowing enforces similar rules at compile time; shared_ptr reference counting has runtime cost and a different philosophy. Modeling ownership inside structs also connects to Rust structs. For basic leak patterns, see the memory leak guide.
shared_ptr destroys its object only when the reference count (the number of shared_ptrs to that object; when it hits 0 the object is deleted) reaches zero. If A points to B and B points back to A, both counts stay ≥1 and neither object is freed. Fix by making one side a weak_ptr, or by designing ownership in one direction only.
The buggy code:
If node1 and node2 point to each other with shared_ptr, node1’s count stays 1 because of node2->prev, and node2’s count stays 1 because of node1->next. When main ends, the local node1 and node2 variables go away, but they still reference each other, so the count never reaches 0 and destructors do not run. Bidirectional links with shared_ptr alone tend to create cycles; one side should be a weak_ptr so it observes without owning.
// After paste: g++ -std=c++17 -o cycle_bad cycle_bad.cpp && ./cycle_bad
#include <memory>
#include <iostream>
class Node {
public:
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // ❌ circular reference!
~Node() { std::cout << "Node destroyed\n"; }
};
int main() {
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2; // node1 → node2
node2->prev = node1; // node2 → node1 (cycle!)
// node1 and node2 reference each other
// reference count never reaches 0
// memory is not freed!
return 0;
}
// "Node destroyed" never prints!
Explanation: node1->next = node2 and node2->prev = node1 make each node’s reference count stay at 1. After main’s locals disappear, the mutual shared_ptrs keep counts above 0, so destructors never run and memory is not released.
Output: Node destroyed does not print (no destructor calls due to the cycle).
Fixed version (paste and run with g++ -std=c++17 -o cycle_ok cycle_ok.cpp && ./cycle_ok):
// After paste: g++ -std=c++17 -o cycle_ok cycle_ok.cpp && ./cycle_ok
#include <memory>
#include <iostream>
class Node {
public:
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // ✅ use weak_ptr!
~Node() { std::cout << "Node destroyed\n"; }
};
int main() {
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1; // weak_ptr does not bump ref count
return 0;
}
// "Node destroyed" prints twice!
Explanation: Making prev a weak_ptr means node2->prev = node1 does not increase node1’s strong reference count. When main ends, node1’s count can reach 0 and it is destroyed first; then node2’s count reaches 0 and it is destroyed—hence two "Node destroyed" lines.
Output: Node destroyed prints twice.
More scenarios
Scenario 2: Entity–component references in a game engine
If an entity holds components with shared_ptr and a component holds its parent entity with shared_ptr, you get a cycle. Make one side weak_ptr or keep ownership one-way.
Scenario 3: Event listener registration
If the subject holds observers with shared_ptr and observers hold the subject with shared_ptr, you get a cycle. Observers should usually hold the subject with weak_ptr.
Scenario 4: Interop with C APIs
When C++ manages memory allocated by a C library with malloc/free, a unique_ptr with a custom deleter (free) gives RAII-safe release.
Scenario 5: Sharing object lifetime across threads
When thread A creates an object and thread B uses it, knowing when to free is hard. shared_ptr gives “last user frees” and is a common pattern for cross-thread lifetime.
The roles of the three smart pointer types:
flowchart LR
subgraph U[unique_ptr]
U1[Single owner]
U2[Move-only]
end
subgraph S[shared_ptr]
S1[Reference count]
S2[Shared ownership]
end
subgraph W[weak_ptr]
W1[Non-owning]
W2[Break cycles]
end
U --> S
S --> W
This article walks through smart pointers with practical examples.
After reading you will:
- Know the differences among
unique_ptr,shared_ptr, andweak_ptr. - Know when to use each smart pointer.
- Know how to fix circular reference issues.
- Use smart pointers effectively in real code. Practical note: This post is based on problems and fixes from large C++ codebases—pitfalls and debugging tips you rarely see in textbooks.
What are smart pointers?
RAII (Resource Acquisition Is Initialization)
Smart pointer types are classes that follow RAII:
- Constructor: acquire resource (allocate memory)
- Destructor: release resource (free memory)
Analogy: a raw pointer where you call
deleteyourself is like a house where you must turn off lights and lock the door; a smart pointer is like a robot that cleans when you leave—you do not need to see the internaldelete, heap memory is gone when scope ends. When unsure: if the answer to “do many places need to own this?” is no, default tounique_ptr.shared_ptrhas counting overhead and cycle risk—use it only when shared ownership is truly required. If one object owns a resource and others only use it,unique_ptrplus (when needed) raw pointers or references is simpler and safer. In the block below,std::make_unique<int>(42)allocates anintwith value 42 on the heap and wraps it in aunique_ptr.ptrexclusively owns that memory, so when the block ends theunique_ptrdestructor callsdelete. Onreturnor exceptions, leaving scope still runs cleanup—no manualdelete, fewer leak and double-delete risks.
{
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// constructor: allocate memory
std::cout << *ptr << std::endl;
// end of scope → destructor → automatic free
}
Explanation: std::make_unique<int>(42) allocates an int on the heap and returns it wrapped in unique_ptr. When ptr goes out of scope (including return and exceptions), the destructor deletes automatically—no manual delete, avoiding leaks and double delete.
Comparing the three smart pointers
| Type | Ownership | Copy | Move | Overhead | Typical use |
|---|---|---|---|---|---|
unique_ptr | Exclusive | ❌ | ✅ | None (~8 bytes) | Default choice |
shared_ptr | Shared | ✅ | ✅ | Yes (~16 bytes + control block) | Multiple owners |
weak_ptr | Non-owning | ✅ | ✅ | Yes (~16 bytes) | Break cycles |
unique_ptr: exclusive ownership
Properties
- Exclusive ownership: only one
unique_ptrowns the object at a time - Non-copyable: copy ctor and copy assignment are deleted
- Movable: ownership transfers with
std::move - Minimal overhead: same size as a raw pointer (~8 bytes) on typical platforms
Basic usage
make_unique<T>(args...) constructs T on the heap and returns a unique_ptr. Prefer it over raw new—on exceptions you avoid leaks, and use get() only when a raw pointer is required (e.g. passing int* to a C API). Never delete a pointer from get() or transfer ownership to another smart pointer while the unique_ptr still owns it—destruction must happen exactly once. For arrays, use make_unique<int[]>(n) and index with [].
#include <memory>
// creation (preferred)
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// creation (legacy, avoid)
std::unique_ptr<int> ptr2(new int(42));
// access
std::cout << *ptr1 << std::endl; // 42 (dereference)
std::cout << ptr1.get() << std::endl; // print address
// array
std::unique_ptr<int[]> arr = std::make_unique<int[]>(100);
arr[0] = 10;
arr[99] = 20;
Explanation: make_unique<int>(42) is a single object; make_unique<int[]>(100) is a dynamic array wrapped in unique_ptr. get() exposes a raw pointer for C APIs—do not delete it or move ownership out from under the unique_ptr. Index arrays with arr[i].
Transferring ownership
unique_ptr is not copyable, only movable. std::move(ptr1) leaves ptr1 null and moves ownership to ptr2—only one owner at a time.
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// ❌ cannot copy
// std::unique_ptr<int> ptr2 = ptr1; // compile error!
// ✅ can move
std::unique_ptr<int> ptr2 = std::move(ptr1);
// now:
// - ptr1 is nullptr
// - ptr2 owns the memory
Explanation: std::move(ptr1) transfers ownership to ptr2; after the move, ptr1 is null and only ptr2 will delete. Copy is disabled so two unique_ptrs cannot own the same memory.
Using them in functions
Transfer ownership (callee takes ownership)
void takeOwnership(std::unique_ptr<int> ptr) {
std::cout << *ptr << std::endl;
// freed when function returns
}
int main() {
auto ptr = std::make_unique<int>(42);
takeOwnership(std::move(ptr)); // transfer ownership
// ptr is now nullptr
// if (ptr) { ... } // false
return 0;
}
Explanation: After takeOwnership(std::move(ptr)), the caller’s ptr is null; the parameter owns the allocation and its destructor runs at the end of takeOwnership.
Keep ownership (read-only)
void useValue(const std::unique_ptr<int>& ptr) {
std::cout << *ptr << std::endl;
// keep ownership, read only
}
int main() {
auto ptr = std::make_unique<int>(42);
useValue(ptr); // ownership stays
// ptr still valid
std::cout << *ptr << std::endl;
return 0;
}
Explanation: const std::unique_ptr<int>& avoids copy/move; the caller’s ptr keeps ownership. Use when you only read the value.
Return ownership
std::unique_ptr<int> createObject() {
return std::make_unique<int>(42);
}
int main() {
auto ptr = createObject(); // receive ownership
std::cout << *ptr << std::endl;
return 0;
}
Explanation: Returning make_unique uses RVO or move; the caller’s ptr owns the object and it is freed when ptr goes out of scope.
Custom deleter
Managing a file handle:
struct FileCloser {
void operator()(FILE* fp) const {
if (fp) {
std::cout << "Closing file\n";
fclose(fp);
}
}
};
void readFile() {
std::unique_ptr<FILE, FileCloser> file(fopen("data.txt", "r"));
if (!file) {
throw std::runtime_error("File not found");
// ✅ safe on exception
}
// read file...
// ✅ fclose on scope exit
}
Explanation: std::unique_ptr<FILE, FileCloser> uses the second template parameter as the deleter. On destruction, FileCloser::operator()(fp) runs instead of delete, calling fclose. Exceptions and early returns still close the handle.
Practical unique_ptr example
class ResourceManager {
std::vector<std::unique_ptr<Resource>> resources;
public:
void addResource(std::unique_ptr<Resource> res) {
resources.push_back(std::move(res));
}
std::unique_ptr<Resource> removeResource(size_t index) {
auto res = std::move(resources[index]);
resources.erase(resources.begin() + index);
return res; // return ownership
}
};
Explanation: addResource moves ownership into the vector; removeResource moves one element out and returns it. When the vector is destroyed, each element’s destructor runs and resources are freed.
shared_ptr: shared ownership and reference counting
Properties
- Shared ownership: many
shared_ptrs can own the same object - Reference counting: an internal count tracks strong references
- Automatic deletion: when the last
shared_ptris destroyed, the object is deleted - Thread-safe counting: reference count updates use atomics
Basic usage
#include <memory>
// creation (preferred)
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
// copy (increments count)
std::shared_ptr<int> ptr2 = ptr1;
std::cout << ptr1.use_count() << std::endl; // 2
// destroy ptr1 (decrement)
ptr1.reset();
std::cout << ptr2.use_count() << std::endl; // 1
// destroy ptr2 (count 0 → free)
ptr2.reset();
Explanation: ptr2 = ptr1 shares the control block; count becomes 2. ptr1.reset() drops one owner; ptr2.reset() drops the last and frees the heap object.
Visualizing reference counts
void demonstrateRefCount() {
auto ptr1 = std::make_shared<int>(42);
std::cout << "Count: " << ptr1.use_count() << std::endl; // 1
{
auto ptr2 = ptr1;
std::cout << "Count: " << ptr1.use_count() << std::endl; // 2
{
auto ptr3 = ptr1;
std::cout << "Count: " << ptr1.use_count() << std::endl; // 3
// ptr3 destroyed
}
std::cout << "Count: " << ptr1.use_count() << std::endl; // 2
// ptr2 destroyed
}
std::cout << "Count: " << ptr1.use_count() << std::endl; // 1
// ptr1 destroyed → count 0 → free
}
Explanation: Nested scopes copy ptr1; each inner scope end destroys a shared_ptr and decrements the count. When only ptr1 remains, count is 1; when ptr1 is destroyed, count hits 0 and memory is freed.
shared_ptr internals
graph LR
subgraph Stack[Stack]
PTR1["ptr1<br/>━━━━━━━━<br/>data ptr<br/>ctrl ptr"]
PTR2["ptr2<br/>━━━━━━━━<br/>data ptr<br/>ctrl ptr"]
end
subgraph Heap[Heap]
CTRL["Control block<br/>━━━━━━━━━━━━<br/>ref count: 2<br/>weak count: 0<br/>deleter"]
OBJ["Object int<br/>━━━━━━━━━━━━<br/>value: 42"]
end
PTR1 -->|data ptr| OBJ
PTR1 -->|ctrl ptr| CTRL
PTR2 -->|data ptr| OBJ
PTR2 -->|ctrl ptr| CTRL
style Stack fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style Heap fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style PTR1 fill:#bbdefb,stroke:#1976d2,stroke-width:2px
style PTR2 fill:#bbdefb,stroke:#1976d2,stroke-width:2px
style CTRL fill:#ffe0b2,stroke:#f57c00,stroke-width:2px
style OBJ fill:#ffe0b2,stroke:#f57c00,stroke-width:2px
shared_ptr use cases
Case 1: graph structure
A graph is where shared_ptr alone goes wrong most easily. Bidirectional edges held as shared_ptr are cycles by definition:
class Node {
public:
int value;
std::vector<std::shared_ptr<Node>> neighbors; // ❌ owning edges in both directions
Node(int v) : value(v) {}
};
int main() {
auto node1 = std::make_shared<Node>(1);
auto node2 = std::make_shared<Node>(2);
node1->neighbors.push_back(node2);
node2->neighbors.push_back(node1); // node1 <-> node2 is a cycle
return 0; // neither Node is destroyed: leaked
}
Explanation: When the locals go away, each node is still owned by the other’s neighbors vector, so both counts stay at 1. The usual fix is to give the nodes a single owner (for example std::vector<std::unique_ptr<Node>> in a Graph object) and store edges as raw pointers or indices, or to keep owning edges in one direction only and make the reverse edges std::weak_ptr<Node>.
Case 2: cache
Resource cache:
class ResourceCache {
std::map<std::string, std::shared_ptr<Resource>> cache;
public:
std::shared_ptr<Resource> get(const std::string& key) {
auto it = cache.find(key);
if (it != cache.end()) {
return it->second; // bumps ref count
}
// cache miss
auto resource = std::make_shared<Resource>(key);
cache[key] = resource;
return resource;
}
void clear() {
cache.clear(); // drop all shared_ptrs → may free resources
}
};
Explanation: On hit, returning it->second shares ownership. On miss, make_shared creates the entry. clear() destroys map entries and decrements counts so unreferenced resources are freed.
Cost of shared_ptr
sizeof(std::unique_ptr<int>); // 8 bytes (pointer only)
sizeof(std::shared_ptr<int>); // 16 bytes (object ptr + control block ptr)
Explanation: unique_ptr is typically one pointer wide; shared_ptr holds object pointer plus control block pointer (ref count, weak count, deleter, etc.). The control block is usually heap-allocated.
Control block (typical):
- Strong ref count (~4 bytes)
- Weak count (~4 bytes)
- Deleter pointer (~8 bytes)
- Allocator bookkeeping (variable) Performance:
- Copy uses atomic increment
- Small overhead in multithreaded code
- Often negligible compared to work in the program
weak_ptr: breaking cycles
Circular references
The introduction showed the core case: two Nodes linked by shared_ptr in both directions keep each other’s strong count at 1, so neither destructor runs, and changing prev to a weak_ptr fixes it because a weak_ptr does not increase the strong count. The rule that generalizes is one owning direction per relationship: parent owns child with shared_ptr (or unique_ptr), the back-pointer to the parent is a weak_ptr (or a raw pointer if the parent is guaranteed to outlive the child).
The cycle patterns that show up in real code (parent/child trees, caches with weak values, observer lists), and how to confirm a cycle with use_count(), Valgrind and ASan’s LeakSanitizer, are covered in C++ Circular References: shared_ptr Leaks and Breaking. This section stays on the weak_ptr API itself.
Using weak_ptr
std::shared_ptr<int> shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;
// weak_ptr cannot be dereferenced directly
// std::cout << *weak << std::endl; // ❌ compile error!
// obtain shared_ptr with lock()
if (auto locked = weak.lock()) {
std::cout << *locked << std::endl; // 42
std::cout << "Object alive\n";
} else {
std::cout << "Object destroyed\n";
}
// destroy shared_ptr
shared.reset();
// weak_ptr object exists but target is gone
if (auto locked = weak.lock()) {
// not taken
} else {
std::cout << "Object destroyed\n"; // prints
}
Explanation: weak_ptr has no operator*; use lock() to get a shared_ptr. If the object lives, lock() returns a non-empty shared_ptr; if it was destroyed, you get empty. After shared.reset(), weak.lock() fails.
weak_ptr cache example
Weak reference cache:
class WeakResourceCache {
std::map<std::string, std::weak_ptr<Resource>> cache;
public:
std::shared_ptr<Resource> get(const std::string& key) {
auto it = cache.find(key);
if (it != cache.end()) {
if (auto resource = it->second.lock()) {
// hit and still alive
return resource;
} else {
// stale entry
cache.erase(it);
}
}
// miss or expired
auto resource = std::make_shared<Resource>(key);
cache[key] = resource; // store weak_ptr
return resource;
}
};
Explanation: Storing weak_ptr lets resources die when no external shared_ptr remains. lock() success returns a live object; failure removes the stale entry. Saves memory without pinning unused objects.
Benefits:
- Keep cache entries while resources are in use
- Free automatically when unused
- Avoid strong-reference cycles
Smart pointer selection guide
Decision flow
Need ownership?
├─ No → raw pointer or reference
└─ Yes → next question
Multiple owners?
├─ No → unique_ptr (default)
└─ Yes → next question
Possible cycle?
├─ No → shared_ptr
└─ Yes → shared_ptr + weak_ptr
Practical rules
When to use unique_ptr (~90%)
// ✅ factory
std::unique_ptr<Widget> createWidget() {
return std::make_unique<Widget>();
}
// ✅ class members
class Window {
std::unique_ptr<Button> closeButton;
std::unique_ptr<TextBox> titleBar;
};
// ✅ containers
std::vector<std::unique_ptr<Shape>> shapes;
Explanation: Single owner → unique_ptr. Factories, exclusive members, vector<unique_ptr<T>>—others observe via get() or references.
When to use shared_ptr (~9%)
// ✅ graphs
class Node {
std::vector<std::shared_ptr<Node>> neighbors;
};
// ✅ cache
std::map<std::string, std::shared_ptr<Data>> cache;
// ✅ async work
void asyncTask(std::shared_ptr<Data> data) {
std::thread([data]() {
// data valid until this thread finishes
processData(data);
}).detach();
}
Explanation: Multiple owners—graphs, caches, data captured by threads. Copying shared_ptr into the lambda keeps the object alive for the thread’s lifetime.
When to use weak_ptr (~1%)
// ✅ break cycles
class Parent {
std::vector<std::shared_ptr<Child>> children;
};
class Child {
std::weak_ptr<Parent> parent; // break cycle
};
// ✅ observer pattern
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
};
Explanation: Parent/child mutual references—make one side weak_ptr. Observers stored weakly avoid dangling strong refs when observers disappear first.
Practical patterns
Pattern 1: factory functions
class ShapeFactory {
public:
static std::unique_ptr<Shape> createCircle(double radius) {
return std::make_unique<Circle>(radius);
}
static std::unique_ptr<Shape> createRectangle(double w, double h) {
return std::make_unique<Rectangle>(w, h);
}
};
int main() {
auto circle = ShapeFactory::createCircle(5.0);
auto rect = ShapeFactory::createRectangle(10.0, 20.0);
circle->draw();
rect->draw();
return 0;
}
Explanation: Returning make_unique gives clear ownership to the caller—good polymorphism with a single owner.
Pattern 2: Pimpl (pointer to implementation)
// Widget.h
class Widget {
public:
Widget();
~Widget();
void doSomething();
private:
class Impl; // forward declaration
std::unique_ptr<Impl> pImpl;
};
// Widget.cpp
class Widget::Impl {
public:
void doSomethingInternal() {
// implementation...
}
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // unique_ptr destroys Impl
void Widget::doSomething() {
pImpl->doSomethingInternal();
}
Explanation: Widget holds unique_ptr<Impl>—header needs only a forward declaration. Implementation stays in .cpp, cutting compile dependencies. User-defined destructor in .cpp may be required for incomplete Impl.
Benefits:
- Hide implementation from headers
- Fewer recompiles
- More stable ABI surface
Pattern 3: polymorphic containers
class Animal {
public:
virtual void speak() = 0;
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof!\n"; }
};
class Cat : public Animal {
public:
void speak() override { std::cout << "Meow!\n"; }
};
int main() {
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>());
animals.push_back(std::make_unique<Cat>());
animals.push_back(std::make_unique<Dog>());
for (const auto& animal : animals) {
animal->speak(); // polymorphism
}
// ✅ all animals freed
return 0;
}
Explanation: vector<unique_ptr<Animal>> gives polymorphism with one owning container; destruction calls the right derived destructors.
Full sketch: entity–component (unique_ptr + shared_ptr + weak_ptr)
// Entity: unique_ptr owns components
// World: shared_ptr manages entities
// ParentRefComponent: weak_ptr to parent (break cycle)
class Entity {
std::vector<std::unique_ptr<Component>> components_;
public:
template<typename T, typename... Args>
T* addComponent(Args&&... args) {
auto c = std::make_unique<T>(std::forward<Args>(args)...);
T* p = c.get();
components_.push_back(std::move(c));
return p;
}
};
// World::createEntity() → make_shared<Entity>()
// ParentRefComponent::parent → weak_ptr<Entity>
Explanation: Components owned by Entity with unique_ptr; World may hold shared_ptr<Entity>; back-references use weak_ptr to avoid cycles.
Pattern 4: observer pattern with weak_ptr
Event system sketch:
class Subject {
std::vector<std::weak_ptr<Observer>> observers;
public:
void attach(std::shared_ptr<Observer> observer) {
observers.push_back(observer);
}
void notify() {
// remove expired observers
observers.erase(
std::remove_if(observers.begin(), observers.end(),
[](const std::weak_ptr<Observer>& wp) {
return wp.expired();
}),
observers.end()
);
// notify live observers
for (auto& wp : observers) {
if (auto observer = wp.lock()) {
observer->update();
}
}
}
};
int main() {
Subject subject;
{
auto observer1 = std::make_shared<Observer>();
subject.attach(observer1);
subject.notify(); // observer1 runs
// observer1 destroyed
}
subject.notify(); // expired observers removed
return 0;
}
Explanation: Storing weak_ptr avoids dangling when an observer is destroyed first. notify() removes expired entries with remove_if, then lock()s the rest and calls update().
Common errors and fixes
Error 1: copying a unique_ptr
Symptom: error: use of deleted function 'std::unique_ptr<T>::unique_ptr(const std::unique_ptr<T>&)'
Cause: copy constructor is deleted.
// ❌ wrong
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = ptr1; // compile error!
Fix:
// ✅ move
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = std::move(ptr1);
Error 2: deleting a pointer from get()
Symptom: double-free crash.
Fix: get() is a non-owning view—never delete. Use only for C APIs or observing.
Error 3: two shared_ptrs from the same raw pointer
Symptom: double delete.
Fix: use make_shared, or one shared_ptr then copies.
Error 4: dereferencing weak_ptr without lock()
Symptom: no operator* on weak_ptr.
Fix: if (auto locked = weak.lock()) { /* use locked */ }
Error 5: “Leak-free” Valgrind but growing memory
Symptom: RSS grows though Memcheck reports no definite leaks (the cycles are still reachable from a live owner, or the process never exits cleanly for Memcheck to report them).
Fix: break cycles—e.g. class B { std::weak_ptr<A> a_; };
Error 6: wrapping this in shared_ptr by hand
Symptom: double delete from std::shared_ptr<MyClass>(this).
Fix: inherit enable_shared_from_this and use shared_from_this() only for objects already managed by shared_ptr.
Error 7: new + shared_ptr instead of make_shared
Symptom: two allocations (object + control block).
Fix: auto p = std::make_shared<Widget>(1, 2, 3);
Performance comparison
unique_ptr vs shared_ptr vs raw pointer
| Aspect | unique_ptr | shared_ptr | raw pointer |
|---|---|---|---|
| Size | ~8 bytes | ~16 bytes | ~8 bytes |
| Allocation overhead | pointer only | control block | pointer only |
| Copy | not allowed (move only) | atomic inc | pointer copy |
| Destruction cost | single delete | dec ref + maybe delete | manual delete |
Micro-benchmark: create/destroy in a loop
// Typical 1M create/destroy loop:
// unique_ptr ~50ms, shared_ptr ~100ms (~1.5–3× slower)
for (int i = 0; i < 1000000; ++i) {
auto p = std::make_unique<int>(42); // or make_shared
}
Expectation: shared_ptr is often ~1.5–3× slower than unique_ptr due to atomics and the control block.
make_shared vs new + shared_ptr
// make_shared: one allocation (better locality)
auto p1 = std::make_shared<LargeObject>(args...);
// new + shared_ptr: two allocations
std::shared_ptr<LargeObject> p2(new LargeObject(args...));
Recommendation: prefer make_shared.
Summary table
| Operation | unique_ptr | shared_ptr | Notes |
|---|---|---|---|
| Create (make_*) | O(1) | O(1) + control block | shared slightly slower |
| Copy | N/A | atomic inc | shared only |
| Move | O(1) | O(1) | both cheap |
| Destroy | O(1) | O(1) + atomic dec | shared slightly heavier |
| Dereference | pointer | pointer | similar |
Conclusion: if there is a single owner, unique_ptr is fastest and smallest. Use shared_ptr only when sharing is required.
Production patterns
Pattern 1: factories return unique_ptr
DocumentFactory::create(path) returning make_unique<Document> makes ownership obvious at the call site.
Pattern 2: members own resources with unique_ptr
DatabaseConnection with unique_ptr<ConnectionHandle> and unique_ptr<StatementCache> auto-cleans on destruction—often no Rule of Five boilerplate.
Pattern 3: cross-thread sharing with shared_ptr
std::thread([config]() { runTask(config); }) copying a shared_ptr into the capture extends lifetime until the thread finishes.
Pattern 4: cache + weak_ptr
Store weak_ptr in the cache; lock() to validate. Return live objects; recreate after expiry.
Pattern 5: Pimpl + unique_ptr (ABI)
Forward-declared Impl and std::unique_ptr<Impl> pImpl_ in the header keep binaries more stable when implementation changes.
Pattern 6: enable_shared_from_this
For async callbacks, shared_from_this() keeps shared_ptr lifetime through completion—never raw this when asynchronous work outlives the call.
Production checklist
- Default to
unique_ptr;shared_ptronly when sharing is real - Prefer
make_unique/make_sharedover rawnew - On bidirectional edges, one side
weak_ptr - Never
deletepointers fromget() - For
thisasshared_ptr, useenable_shared_from_this - On hot paths, prefer
unique_ptrfirst
Closing
Takeaways
✅ unique_ptr: default choice (~90% of cases)
- Exclusive ownership
- Minimal overhead
- Move-only ✅ shared_ptr: when sharing is needed (~9%)
- Multiple owners
- Reference counting
- Some overhead ✅ weak_ptr: break cycles (~1%)
- Non-owning observation
- Access via
lock() - Observers, caches
Rules of thumb
- Default to
unique_ptr(when in doubt, start there) shared_ptronly when sharing is truly required- Bidirectional references →
weak_ptron one side - Raw pointers for non-owning views only
Lessons learned
- Cycles are silent (Valgrind may not flag them as classic leaks)
weak_ptris essential for bidirectional structures- Prefer safety over micro-optimization (most of the time)
Next
Learn RAII—the foundation of smart pointers—to manage every kind of resource safely, not just memory.
FAQ
Q. Valgrind reports no leaks, but memory keeps growing. Could smart pointers be the cause?
A. Yes, if two objects hold shared_ptrs to each other. The reference counts never reach zero, so the objects are never freed. While something long-lived (a manager, registry or cache) still points into the cycle, Memcheck classifies those blocks as “still reachable”, not as a leak; only once nothing outside the cycle refers to it does Memcheck report it as definitely/indirectly lost at exit. Make one side of the relationship (usually the back-pointer to a parent or owner) a weak_ptr and call lock() when you need to use it.
Q. Why does creating two shared_ptrs from the same raw pointer crash?
A. Each shared_ptr constructed from a raw pointer creates its own control block, so both believe they are the sole owner and both delete the object: a double delete. Create the object with make_shared (or one shared_ptr) and copy that; inside a class, use enable_shared_from_this and shared_from_this() instead of shared_ptr<T>(this).
One-liner: use unique_ptr, shared_ptr, and weak_ptr to express ownership and break cycles—then move on to RAII (#6-4).
References
Official docs
Further reading
- C++ Core Guidelines - Smart Pointers
- “Effective Modern C++” by Scott Meyers (Items 18–22)
Related Articles
- C++ stack vs heap | recursion crashes and stack overflow
- Tracking Down C++ Memory Leaks: Five Dangerous new/delete Patterns, Valgrind and ASan
- C++ RAII Explained | Why Constructors and Destructors Prevent Leaks
- C++ Circular References: shared_ptr Leaks and Breaking
- C++ shared_ptr vs unique_ptr: Smart Pointer Choice Complete