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): A std::unique_ptr maintains strict, singular ownership of the underlying raw pointer. No two std::unique_ptr instances can manage the same resource simultaneously. Because of this, it cannot be copied; it can only be moved using std::move(), transferring ownership from one instance to another. Once a std::unique_ptr goes out of scope or is reset, the underlying resource is immediately destroyed.
  • std::shared_ptr (Shared Ownership): A std::shared_ptr allows multiple pointers to own the same resource simultaneously. It tracks the number of active owners using an internal reference counter. Copying a std::shared_ptr increments 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_ptr has 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: A std::shared_ptr is 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 a std::unique_ptr.
  • Use std::make_shared<T>() to create a std::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.