Skip to content

Latest commit

 

History

History
298 lines (233 loc) · 15.8 KB

smart-pointers.md

File metadata and controls

298 lines (233 loc) · 15.8 KB

Smart Pointers


Executive Abstract

Smart Pointers are class templates (std::unique_ptr, std::shared_ptr, std::weak_ptr) that encapsulate raw memory pointers behind Resource Acquisition Is Initialization (RAII) semantics. Introduced in C++11 and refined across subsequent standards, smart pointers automate dynamic memory lifetime management, establish explicit ownership semantics (exclusive vs. shared vs. non-owning observer), eliminate raw new/delete calls, prevent memory leaks and Use-After-Free vulnerabilities, and permanently replace the unsafe C++98 std::auto_ptr.


Motivation & Historical Context

Pre-C++11 Deficiencies: Raw Memory & std::auto_ptr Vulnerabilities

In C++98/C++03, managing heap memory relied on raw pointers and explicit pairs of new and delete:

// C++98 Raw Pointer Memory Leak Trap
void execute_transaction() {
    Widget* w = new Widget();
    if (!process(w)) {
        return; // BUG: Memory leak! 'delete w' is bypassed!
    }
    delete w;
}

Why C++98 std::auto_ptr Failed:

  1. Hijacked Copy Semantics: std::auto_ptr<T> attempted to implement ownership transfer via copy operations (std::auto_ptr<T> p2 = p1;). Calling the copy constructor mutated p1, setting its inner pointer to null!
  2. STL Container Crashes: Passing std::auto_ptr into STL algorithms (such as std::sort or std::vector::push_back) caused silent null pointer dereferences and crashes because algorithm internal copy steps stripped element pointers.
  3. No Array Support: std::auto_ptr called scalar delete instead of array delete[], resulting in undefined behavior when managing heap arrays (new T[N]).

Solution: C++11 deprecated std::auto_ptr (removed completely in C++17) and introduced a taxonomy of smart pointer types backed by modern C++ move semantics.


Technical Mechanics & Taxonomy of Ownership

                               Smart Pointer Ownership Models
                                             │
      ┌──────────────────────────────────────┼──────────────────────────────────────┐
      ▼                                      ▼                                      ▼
1. std::unique_ptr<T>                  2. std::shared_ptr<T>                 3. std::weak_ptr<T>
Exclusive Ownership                    Shared Reference Counted              Non-Owning Observer
      │                                      │                                      │
      ▼                                      ▼                                      ▼
Move-Only (Non-Copyable)               Copyable / Moveable                   Copyable / Moveable
Zero Memory Overhead                   Allocates Control Block               Prevents Cyclic Deadlocks

1. std::unique_ptr<T, Deleter> (Exclusive Ownership)

  • Ownership Model: Strictly enforces single, exclusive ownership of a dynamic resource.
  • Copy & Move Semantics: Non-copyable (Copy Constructor = delete). Move-Only via std::move.
  • Memory Overhead: Zero overhead over raw pointers (sizeof(std::unique_ptr<T>) == sizeof(T*)) when using default stateless deleters.
  • Array Specialization: std::unique_ptr<T[]> provides native operator[] indexing and executes delete[] on destruction.
  • Factory Function: std::make_unique<T>(args...) (C++14).

2. std::shared_ptr<T> (Shared Ownership)

  • Ownership Model: Shares resource ownership among multiple std::shared_ptr instances.
  • The Control Block: Every std::shared_ptr points to both the managed object $T$ and a heap-allocated Control Block:
 ┌──────────────────────────┐             ┌────────────────────────────────────────────────────────┐
 │   std::shared_ptr<T>     │             │                     CONTROL BLOCK                      │
 ├──────────────────────────┤             ├───────────────────────────┬────────────────────────────┤
 │ T* ptr                   ├────────────►│ Managed Object T          │ Strong Count (use_count=2) │
 │ ControlBlock* ctrl_block ├──────┐      ├───────────────────────────┼────────────────────────────┤
 └──────────────────────────┘      │      │ Custom Deleter / Alloc    │ Weak Count (weak_count=1)  │
                                   └─────►└───────────────────────────┴────────────────────────────┘
  1. Strong Reference Count (use_count): Incremented on copy; decremented on destruction. When use_count reaches 0, the managed object $T$ is destroyed (~T()).
  2. Weak Reference Count (weak_count): Incremented by active std::weak_ptr observers. The Control Block itself remains allocated until both use_count and weak_count reach 0.
  • Thread Safety: Reference counter operations (++ and --) are executed using atomic instructions (std::atomic). Modifying the reference count is thread-safe; mutating the underlying target object $T$ requires explicit synchronization.
  • Factory Function: std::make_shared<T>(args...) allocates the Control Block and object $T$ in a single contiguous memory allocation, improving L1 cache locality and reducing allocator overhead.

3. std::weak_ptr<T> (Non-Owning Observer)

  • Ownership Model: Observes a resource managed by std::shared_ptr without incrementing use_count.
  • Cyclic Dependency Resolution: Breaks Reference Cycles in graph or parent-child structures (where two shared_ptr instances point to each other, preventing use_count from ever reaching zero).
  • Access Mechanism: std::weak_ptr cannot be dereferenced directly (operator-> absent). To access the underlying resource safely, call .lock(), which returns a valid std::shared_ptr<T> if the object is still alive, or nullptr if it has been destroyed.

Evolution Across Standards (C++11 to C++26/C++29)

┌──────────────────────────────────────────────────────────────────────┐
│ C++11                                                                │
├──────────────────────────────────────────────────────────────────────┤
│ • Introduced std::unique_ptr, std::shared_ptr, std::weak_ptr         │
│ • Introduced std::make_shared<T>() factory function                  │
│ • Deprecated legacy std::auto_ptr                                    │
└──────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────────┐
│ C++14 / C++17                                                        │
├──────────────────────────────────────────────────────────────────────┤
│ • std::make_unique<T>() introduced (C++14)                           │
│ • Completely removed std::auto_ptr from Standard Library (C++17)     │
│ • Array support for shared_ptr: std::shared_ptr<T[]>                 │
└──────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────────┐
│ C++20                                                                │
├──────────────────────────────────────────────────────────────────────┤
│ • Atomic Smart Pointers: std::atomic<std::shared_ptr<T>>             │
│ • std::make_shared_for_overwrite for uninitialized buffer allocations│
└──────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────────┐
│ C++26 / C++29                                                        │
├──────────────────────────────────────────────────────────────────────┤
│ • Hazard Pointers (<hazard_pointer>) and Read-Copy-Update (<rcu>)    │
│   provide ultra-fast lock-free alternatives to shared_ptr            │
└──────────────────────────────────────────────────────────────────────┘

Comprehensive Code Examples

Example 1: Exclusive Ownership & Custom Deleters (std::unique_ptr)

Demonstrates move-only exclusive resource management and custom cleanup callbacks (e.g., C-file handles or socket closing).

#include <iostream>
#include <memory>
#include <cstdio>

// Custom RAII Deleter for C FILE* handles
struct FileCloser {
    void operator()(std::FILE* fp) const noexcept {
        if (fp) {
            std::cout << "  [Custom Deleter] Closing File Handle...\n";
            std::fclose(fp);
        }
    }
};

using UniqueFile = std::unique_ptr<std::FILE, FileCloser>;

int main() {
    // 1. Standard unique_ptr with make_unique (C++14)
    auto int_ptr = std::make_unique<int>(42);

    // std::unique_ptr is MOVE-ONLY!
    auto moved_ptr = std::move(int_ptr); // int_ptr is now nullptr
    std::cout << "Moved Pointer Value: " << *moved_ptr << "\n";

    // 2. Custom Deleter management
    {
        UniqueFile file(std::fopen("temp.txt", "w"));
        if (file) {
            std::fputs("C++ Smart Pointers", file.get());
        }
    } // FileCloser automatically executes std::fclose here at scope exit!

    return 0;
}

Example 2: Shared Ownership & Cyclic Reference Resolution (std::weak_ptr)

Demonstrates how std::weak_ptr breaks reference cycles that would otherwise cause permanent memory leaks.

#include <iostream>
#include <memory>
#include <string>

struct Node {
    std::string name;
    std::shared_ptr<Node> next;

    // CRITICAL: Parent back-reference MUST be std::weak_ptr!
    // Using std::shared_ptr here would create a Reference Cycle (Leak)!
    std::weak_ptr<Node> parent;

    explicit Node(std::string n) : name(std::move(n)) {
        std::cout << "  [Constructed] Node: " << name << "\n";
    }

    ~Node() {
        std::cout << "  [Destroyed] Node: " << name << "\n";
    }
};

int main() {
    std::cout << "--- Creating Node Tree ---\n";
    auto parent_node = std::make_shared<Node>("Parent");
    auto child_node  = std::make_shared<Node>("Child");

    // Establish bidirectional relationships
    parent_node->next = child_node;
    child_node->parent = parent_node; // Weak reference does NOT increment use_count!

    std::cout << "Parent use_count: " << parent_node.use_count() << "\n"; // 1
    std::cout << "Child use_count:  " << child_node.use_count() << "\n";  // 1

    // Access parent safely from child via weak_ptr lock()
    if (auto parent_sp = child_node->parent.lock()) {
        std::cout << "Child safely accessed Parent: " << parent_sp->name << "\n";
    }

    std::cout << "--- Exiting Scope (Automatic Cleanup) ---\n";
    return 0; // Both nodes destroyed cleanly! Zero memory leaks!
}

Example 3: Modern C++20 Thread-Safe Atomic Smart Pointers

Demonstrates lock-free thread-safe updates to global smart pointers using C++20 std::atomic<std::shared_ptr<T>>.

#include <iostream>
#include <memory>
#include <atomic>
#include <thread>

struct Configuration {
    int version;
};

// C++20 Thread-Safe Atomic Shared Pointer
std::atomic<std::shared_ptr<Configuration>> g_config;

void update_configuration(int new_version) {
    auto new_config = std::make_shared<Configuration>(Configuration{new_version});
    // Atomic exchange of shared_ptr reference!
    g_config.store(new_config, std::memory_order_release);
}

void read_configuration() {
    // Atomic load of shared_ptr reference!
    std::shared_ptr<Configuration> current = g_config.load(std::memory_order_acquire);
    if (current) {
        std::cout << "Reader Thread Active Config Version: " << current->version << "\n";
    }
}

int main() {
    update_configuration(1);

    std::thread writer(update_configuration, 2);
    std::thread reader(read_configuration);

    writer.join();
    reader.join();

    return 0;
}

Engineering Best Practices & Common Pitfalls

  1. Default to std::unique_ptr:

    • Always choose std::unique_ptr by default. Only upgrade to std::shared_ptr if multiple components genuinely share dynamic object lifetime ownership.
  2. Always Use make Factory Functions:

    • Prefer std::make_unique (C++14) and std::make_shared (C++11) over raw new. They prevent memory leaks caused by unspecified sub-expression evaluation order during function calls.
  3. The "Double Control Block" Trap:

    • Creating two separate std::shared_ptr instances from a single raw pointer creates two independent Control Blocks:
      Widget* raw = new Widget();
      std::shared_ptr<Widget> p1(raw);
      std::shared_ptr<Widget> p2(raw); // DISASTER! Double-Free Undefined Behavior!
    • Fix: If a class method needs to pass a std::shared_ptr to itself (this), inherit from std::enable_shared_from_this<T> and call shared_from_this().

Relationships

  • Related to:
  • Superseded by: None.
  • Supersedes: Raw memory management (new/delete) and legacy C++98 std::auto_ptr.

Back to C++11 section.