Every C++ developer learns about
unique_ptrandshared_ptr. But most tutorials skip the part that actually matters in production: when each one costs you performance, and why.
The One Rule
Before anything else, memorize this:
Who owns this resource?
├── One owner → std::unique_ptr (DEFAULT)
├── Multiple owners → std::shared_ptr (RARE)
├── Non-owning observer →
│ ├── Of shared_ptr → std::weak_ptr
│ └── Of anything → raw pointer or reference
└── C API resource → std::unique_ptr + custom deleter
If you’re unsure, start with unique_ptr. You’re right 90% of the time.
unique_ptr — The One You Should Use Everywhere
unique_ptr expresses sole ownership. One pointer owns the object. When it goes out of scope, the object is deleted. No questions asked.
Why It’s Zero-Cost
static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*)); // Both 8 bytesThat’s not a simplification — unique_ptr is literally the same size as a raw pointer. The compiler optimizes away all the abstraction. The destructor call becomes a single delete instruction. The operator-> compiles to a direct dereference. Zero overhead.
This is why I used raw pointers with a custom arena allocator in my HFT Matching Engine — but for any normal application, unique_ptr gives you that same raw pointer performance with automatic cleanup.
The Move-Only Trick
auto p1 = std::make_unique<Order>(10000, 50);
auto p2 = p1; // ❌ COMPILE ERROR! Can't copy unique_ptr
auto p2 = std::move(p1); // ✅ Ownership transferred. p1 is now null.This is a compile-time guarantee that only one pointer owns the object. No reference counting. No runtime checks. The language enforces it.
Factory Pattern
This is the most common use case — and the one you should use in every project:
std::unique_ptr<Strategy> create_strategy(const Config& cfg) {
if (cfg.type == "momentum")
return std::make_unique<MomentumStrategy>(cfg);
else
return std::make_unique<MeanReversionStrategy>(cfg);
}
auto strategy = create_strategy(config);
strategy->execute(tick); // Polymorphic call, zero overhead
// Automatically deleted when strategy goes out of scopeCompare this to the raw pointer version:
Strategy* create_strategy(const Config& cfg) {
return new MomentumStrategy(cfg); // Who deletes this? When? What if we throw?
}Same performance. But the raw pointer version leaks memory if you forget to delete. The unique_ptr version literally cannot leak.
Custom Deleters (C API Integration)
This is where unique_ptr really shines — wrapping C resources:
// File handle — auto-closes when done
auto file = std::unique_ptr<FILE, decltype(&fclose)>(
fopen("data.csv", "r"), fclose
);
// No need to remember fclose() — RAII handles it
// Database connection — auto-disconnects
auto db = std::unique_ptr<PGconn, decltype(&PQfinish)>(
PQconnectdb("host=localhost"), PQfinish
);Every C library that gives you a handle + cleanup function can be wrapped in unique_ptr. Memory leaks become impossible.
shared_ptr — The One You Probably Don’t Need
shared_ptr uses reference counting — it tracks how many pointers share ownership. The object is deleted when the last one dies.
How It Works Internally
auto sp1 = std::make_shared<Order>(100.0, 500);
auto sp2 = sp1; // Reference count: 2
┌─────────────────────────┐
│ Control Block │
│ strong_count = 2 │ ← Atomic! (thread-safe)
│ weak_count = 0 │
│ ┌─────────────────────┐ │
│ │ Order object │ │ ← make_shared puts both in ONE allocation
│ │ price = 100.0 │ │
│ │ qty = 500 │ │
│ └─────────────────────┘ │
└─────────────────────────┘
The Hidden Costs
Here’s what tutorials don’t tell you:
Cost #1: Size — 2x larger than unique_ptr
sizeof(std::unique_ptr<int>) == 8; // One pointer
sizeof(std::shared_ptr<int>) == 16; // Two pointers (object + control block)Cost #2: Atomic operations on every copy and destroy
auto sp2 = sp1; // Atomic increment of strong_count (~10-20 ns)
// In a tight loop with millions of copies, this adds up FASTCost #3: Cache line bouncing in multithreaded code
When two threads copy/destroy shared_ptr to the same object, they both need to atomically modify the same strong_count integer. This causes the cache line containing the control block to bounce between CPU cores — a hidden performance killer in concurrent systems.
This is why you’ll never see shared_ptr in HFT engines or game engines on the hot path.
When shared_ptr IS the Right Choice
Genuine shared ownership is rare, but it exists:
// Observer pattern: multiple widgets reference the same data
class Dashboard {
std::shared_ptr<MarketData> data_; // Dashboard shares data...
};
class AlertSystem {
std::shared_ptr<MarketData> data_; // ...with AlertSystem
};
// Data is freed when BOTH Dashboard and AlertSystem are destroyedIf you can’t answer “who should delete this object?” with a single clear answer, shared_ptr is correct. But if you can — use unique_ptr.
weak_ptr — The Cycle Breaker
weak_ptr observes a shared_ptr without incrementing the reference count. It exists for one reason: preventing circular references.
The Classic Memory Leak
class A {
std::shared_ptr<B> b; // A → B (strong)
};
class B {
std::shared_ptr<A> a; // B → A (strong) ← CYCLE!
};
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b; // b: strong_count = 2
b->a = a; // a: strong_count = 2
// Both go out of scope → strong_count drops to 1 → neither deleted → LEAK!The Fix
class B {
std::weak_ptr<A> a; // B → A (weak) — doesn't prevent A's destruction
};
// Now when a goes out of scope → strong_count = 0 → A destroyed → b freedSafe Access Pattern
std::weak_ptr<Order> wp = some_shared_ptr;
if (auto locked = wp.lock()) {
// locked is a shared_ptr — object is alive
std::cout << locked->price << "\n";
} else {
// Object has been destroyed
}Real Use Case: Cache
class PriceCache {
std::unordered_map<std::string, std::weak_ptr<PriceData>> cache_;
public:
std::shared_ptr<PriceData> get(const std::string& symbol) {
if (auto it = cache_.find(symbol); it != cache_.end()) {
if (auto sp = it->second.lock()) return sp; // Cache hit
cache_.erase(it); // Expired — clean up
}
auto data = std::make_shared<PriceData>(fetch(symbol));
cache_[symbol] = data;
return data;
}
};The cache holds weak_ptr, so it doesn’t keep data alive unnecessarily. When no one else references a price, it’s automatically freed and the cache entry becomes expired.
The Parameter Passing Cheat Sheet
This is the part most people get wrong:
// ✅ Function just READS data — pass by const reference
void print(const Order& order);
// ✅ Function MODIFIES data — pass by reference
void update(Order& order);
// ✅ Function TAKES ownership — pass unique_ptr by value
void consume(std::unique_ptr<Order> order);
// ✅ Function SHARES ownership — pass shared_ptr by value
void share(std::shared_ptr<Order> order);
// ❌ DON'T do this — pointless indirection
void f(const std::unique_ptr<Order>& p); // Just use const Order&
void f(const std::shared_ptr<Order>& p); // Just use const Order&The rule: Smart pointers in function signatures mean ownership semantics. If the function doesn’t change ownership, just pass const T&.
Performance Summary
unique_ptr | shared_ptr | Raw pointer | |
|---|---|---|---|
| Size | 8 bytes | 16 bytes | 8 bytes |
| Dereference | 0 ns | 0 ns | 0 ns |
| Copy | ❌ (compile error) | 10-20 ns (atomic) | 0 ns |
| Move | 0 ns | 0 ns | 0 ns |
| Destroy | ~1 ns (delete) | ~10 ns (atomic + delete) | manual |
| Thread safety | N/A | Control block only | N/A |
| Use when | Default | Shared ownership | Performance-critical hot paths |
TL;DR
- Use
unique_ptrby default. It’s zero-cost and prevents leaks. - Use
shared_ptronly for genuine shared ownership. It’s not free — atomic ops on every copy. - Use
weak_ptrto break cycles and for caches. - Pass by
const T&in function parameters — not by smart pointer, unless you’re transferring ownership. - In hot paths (game loops, HFT matching, real-time audio): profile first, but consider arena allocators with raw pointers if
shared_ptratomics show up in your profile.
Follow me for weekly deep dives into C++ systems programming, low-latency architecture, and high-performance computing.
Tags: #cpp #smart-pointers #memory-management #performance #systems-programming