std::unique_ptr vs std::shared_ptr in C++
In modern C++, smart pointers automate memory management to eliminate
manual deallocation and prevent memory leaks. The primary distinction
between std::unique_ptr and std::shared_ptr
lies in their ownership semantics: std::unique_ptr
implements exclusive ownership where a single pointer manages a resource
with zero runtime overhead, whereas std::shared_ptr
implements shared ownership via reference counting, allowing multiple
owners to coordinate the lifetime of a single resource. This article
breaks down their core differences in ownership, performance, custom
deletion, and practical usage.
Ownership Model
std::unique_ptr(Exclusive Ownership): Astd::unique_ptrmaintains strict, singular ownership of the underlying raw pointer. No twostd::unique_ptrinstances can manage the same resource simultaneously. Because of this, it cannot be copied; it can only be moved usingstd::move(), transferring ownership from one instance to another. Once astd::unique_ptrgoes out of scope or is reset, the underlying resource is immediately destroyed.std::shared_ptr(Shared Ownership): Astd::shared_ptrallows multiple pointers to own the same resource simultaneously. It tracks the number of active owners using an internal reference counter. Copying astd::shared_ptrincrements this counter, and destroying an instance or reassigning it decrements it. The managed object is deleted only when the reference counter drops to zero.
Memory Footprint and Performance
std::unique_ptr: With the default deleter,std::unique_ptrhas the exact same size and memory footprint as a raw pointer. It introduces no runtime overhead, requires no heap allocation for control structures, and involves no thread-synchronization costs. It represents a zero-cost abstraction over raw pointers.std::shared_ptr: Astd::shared_ptris typically twice the size of a raw pointer because it stores two pointers: one pointing to the managed object and another pointing to an auxiliary heap-allocated "control block." The control block stores the strong reference count, the weak reference count, and any custom deleter or allocator. Modifying the reference count requires atomic operations to ensure thread safety across concurrent reads and copies, introducing CPU overhead.
Custom Deleters
std::unique_ptr: The type of the custom deleter is encoded directly into the pointer's type signature (e.g.,std::unique_ptr<FILE, decltype(&fclose)>). Passing a stateful deleter increases the overall size of the smart pointer, but because the deleter is bound at compile time, calls to it can be inlined by the compiler for optimal performance.std::shared_ptr: The custom deleter is type-erased and stored inside the dynamically allocated control block. Consequently, the deleter does not alter the type signature of the pointer (e.g.,std::shared_ptr<FILE>), making different instances with different deleters interchangeable in containers and interfaces.
Creation and Best Practices
Modern C++ provides dedicated utility functions for initializing both types safely:
- Use
std::make_unique<T>()to create astd::unique_ptr. - Use
std::make_shared<T>()to create astd::shared_ptr. This optimizes allocation by combining the managed object and the control block into a single contiguous memory block.
Summary of Differences
| Feature | std::unique_ptr |
std::shared_ptr |
|---|---|---|
| Ownership | Exclusive (single owner) | Shared (multiple owners) |
| Copyable | No (move-only) | Yes |
| Memory Size | Equal to a raw pointer | Typically \(2 \times\) raw pointer size |
| Runtime Cost | Zero overhead | Atomic reference counter updates |
| Deleter Type | Part of the template type | Type-erased (stored in control block) |
As a general guideline, prefer std::unique_ptr by
default. It provides clear ownership semantics and maximum efficiency.
Escalate to std::shared_ptr only when an object genuinely
requires shared, distributed lifetime management across independent
subsystems.