Every C++ developer learns about unique_ptr and shared_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 bytes

That’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 scope

Compare 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 FAST

Cost #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 destroyed

If 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 freed

Safe 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_ptrshared_ptrRaw pointer
Size8 bytes16 bytes8 bytes
Dereference0 ns0 ns0 ns
Copy❌ (compile error)10-20 ns (atomic)0 ns
Move0 ns0 ns0 ns
Destroy~1 ns (delete)~10 ns (atomic + delete)manual
Thread safetyN/AControl block onlyN/A
Use whenDefaultShared ownershipPerformance-critical hot paths

TL;DR

  1. Use unique_ptr by default. It’s zero-cost and prevents leaks.
  2. Use shared_ptr only for genuine shared ownership. It’s not free: atomic ops on every copy.
  3. Use weak_ptr to break cycles and for caches.
  4. Pass by const T& in function parameters, not by smart pointer, unless you’re transferring ownership.
  5. In hot paths (game loops, HFT matching, real-time audio): profile first, but consider arena allocators with raw pointers if shared_ptr atomics 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