C++ for C Developers | Lesson 11: Smart Pointers Deep Dive (unique_ptr, shared_ptr, weak_ptr)

Goal: Understand the different ownership models provided by C++ smart pointers (unique_ptr, shared_ptr) and learn how weak_ptr solves the problem of ownership cycles. Reinforce how smart pointers automate RAII for dynamic memory.

flowchart TD
    A1["std::unique_ptr"]
    A2["std::shared_ptr"]
    A3["std::weak_ptr"]

    A1 -->|"Exclusive Owner\n(1 owner)"| B1["Object"]
    A2 -->|"Shared Owner\n(N owners)"| B2["Object"]
    A3 -->|"Non-owning Observer\n(0 ref count change)"| B2

1. Recap: RAII and the Need for Smart Pointers

As we’ve seen, RAII (Resource Acquisition Is Initialization) is fundamental to C++. We tie resource lifetimes (like dynamically allocated memory) to the lifetime of stack-based objects using constructors and destructors. Smart pointers are library classes (in <memory>) that implement RAII specifically for pointers obtained from new. They automatically call delete or delete[] when they go out of scope or are reset.

2. std::unique_ptr: Exclusive Ownership (Review)

flowchart LR
    A["unique_ptr widgetOwner1"]
    B["std::move(widgetOwner1)"]
    C["unique_ptr widgetOwner2"]

    A --> B
    B --> C
    A -.->|"Now nullptr"| D["widgetOwner1 == nullptr"]
  • Concept: Represents exclusive ownership. Only one unique_ptr can own a dynamically allocated object at any given time. When the unique_ptr is destroyed, it deletes the object it owns.
  • Efficiency: Very lightweight, often has no performance overhead compared to a raw pointer.
  • Transfer: Ownership can be transferred (not copied) using std::move(). After moving, the original unique_ptr becomes empty (points to nullptr).
  • Use Cases: Default choice when you need heap allocation but only need one clear owner (e.g., Pimpl idiom, factory functions returning heap objects, managing single resources within a class).
C++
#include <iostream>
#include <memory> // Include header for smart pointers
#include <utility> // For std::move

class Widget {
    int id_;
public:
    Widget(int id) : id_(id) { std::cout << "Widget " << id_ << " Created\n"; }
    ~Widget() { std::cout << "Widget " << id_ << " Destroyed\n"; }
    void identify() const { std::cout << "I am Widget " << id_ << "\n"; }
};

void processWidget(std::unique_ptr<Widget> w_ptr) {
    std::cout << "Processing Widget in function...\n";
    if (w_ptr) { // Check if it's not null
        w_ptr->identify();
    }
    std::cout << "Widget leaves processWidget function scope...\n";
    // w_ptr is destroyed here, deleting the Widget
}

int main() {
    // Create a unique_ptr owning a Widget
    std::unique_ptr<Widget> widgetOwner1 = std::make_unique<Widget>(1); // C++14 way, preferred
    // Or: std::unique_ptr<Widget> widgetOwner1(new Widget(1)); // C++11 way

    widgetOwner1->identify();

    // std::unique_ptr<Widget> widgetOwner2 = widgetOwner1; // ERROR! Cannot copy unique_ptr

    // Transfer ownership
    std::cout << "Transferring ownership...\n";
    std::unique_ptr<Widget> widgetOwner2 = std::move(widgetOwner1); // widgetOwner1 is now empty (null)

    std::cout << "widgetOwner1 is now " << (widgetOwner1 ? "valid" : "empty") << std::endl;
    if (widgetOwner2) {
         widgetOwner2->identify();
    }

    // Transfer ownership into a function (function takes ownership)
    std::cout << "Passing ownership to function...\n";
    processWidget(std::move(widgetOwner2)); // widgetOwner2 becomes empty
    std::cout << "widgetOwner2 is now " << (widgetOwner2 ? "valid" : "empty") << std::endl;

    std::cout << "Main function ends.\n";
    return 0;
}

3. std::shared_ptr: Shared Ownership

flowchart LR
    SP1["shared_ptr sp1"]
    SP2["shared_ptr sp2 = sp1"]
    SP3["shared_ptr sp3 = sp2"]
    OBJ["Node Object\nuse_count: 3"]

    SP1 --> OBJ
    SP2 --> OBJ
    SP3 --> OBJ
  • Concept: Allows multiple shared_ptr instances to co-own the same dynamically allocated object. It uses reference counting to manage the object’s lifetime.
  • Reference Counting: An internal counter keeps track of how many shared_ptrs are currently pointing to the object.
    • When a shared_ptr is copied, the count increases.
    • When a shared_ptr is destroyed (or reset, or assigned a different pointer), the count decreases.
    • The managed object is deleted only when the reference count drops to zero.
  • Creation:
    • std::make_shared<T>(args...): Strongly preferred. More efficient (allocates memory for the object and the reference count control block together) and safer in complex expressions involving exceptions.
    • std::shared_ptr<T>(new T(args...)): Less efficient (two allocations), potentially less safe. Avoid unless necessary (e.g., adopting existing raw pointer).
  • Use Cases: When an object’s lifetime isn’t tied to a single scope or owner. Multiple parts of the program need access and need to keep the object alive as long as any of them still need it (e.g., data shared between data structures, objects in callbacks).
C++
#include <iostream>
#include <memory>
#include <string>
#include <vector>

int main() {
    std::shared_ptr<Widget> sharedW1; // Starts empty

    std::cout << "Creating shared Widget 10...\n";
    sharedW1 = std::make_shared<Widget>(10); // Create using make_shared
    std::cout << "sharedW1 use_count: " << sharedW1.use_count() << std::endl; // Output: 1

    { // Inner scope
        std::cout << "  Entering inner scope.\n";
        std::shared_ptr<Widget> sharedW2 = sharedW1; // Copy shared_ptr
        std::cout << "  sharedW1 use_count: " << sharedW1.use_count() << std::endl; // Output: 2
        std::cout << "  sharedW2 use_count: " << sharedW2.use_count() << std::endl; // Output: 2

        sharedW2->identify(); // Both pointers point to the same object

        std::cout << "  Leaving inner scope.\n";
        // sharedW2 goes out of scope, reference count decreases
    } // Destructor for sharedW2 runs here

    std::cout << "Back in outer scope.\n";
    std::cout << "sharedW1 use_count: " << sharedW1.use_count() << std::endl; // Output: 1
    sharedW1->identify();

    // Assigning nullptr or resetting decreases count
    sharedW1.reset(); // Or sharedW1 = nullptr;
    std::cout << "After reset, sharedW1 use_count: " << sharedW1.use_count() << std::endl; // Output: 0
    // Widget 10 is destroyed HERE because count reached 0

    // Danger Zone: Don't create multiple shared_ptrs from the same raw pointer!
    Widget* raw_ptr = new Widget(20);
    // std::shared_ptr<Widget> danger1(raw_ptr); // Owns raw_ptr, ref count = 1
    // std::shared_ptr<Widget> danger2(raw_ptr); // ALSO owns raw_ptr, different ref count = 1
    // When danger1 goes out of scope, it deletes raw_ptr.
    // When danger2 goes out of scope, it tries to delete raw_ptr AGAIN! -> CRASH!
    // ALWAYS create from make_shared or create ONE shared_ptr from new and COPY that one.
    delete raw_ptr; // Manual delete needed here since we didn't use smart pointers correctly above

    return 0;
}

4. The shared_ptr Cycle Problem

Reference counting fails if objects form a cycle where they hold shared_ptrs to each other.

C++
#include <iostream>
#include <memory>

struct Node {
    int id;
    std::shared_ptr<Node> other; // Points to another Node

    Node(int i) : id(i) { std::cout << "Node " << id << " Created\n"; }
    ~Node() { std::cout << "Node " << id << " Destroyed\n"; }
};

int main() {
    std::shared_ptr<Node> n1 = std::make_shared<Node>(1); // n1 count = 1
    std::shared_ptr<Node> n2 = std::make_shared<Node>(2); // n2 count = 1

    std::cout << "Creating cycle...\n";
    n1->other = n2; // n2 count = 2
    n2->other = n1; // n1 count = 2

    std::cout << "n1 use_count: " << n1.use_count() << std::endl; // Output: 2
    std::cout << "n2 use_count: " << n2.use_count() << std::endl; // Output: 2

    std::cout << "Leaving main scope...\n";
    // n1 goes out of scope, its count decreases to 1 (because n2->other still holds it)
    // n2 goes out of scope, its count decreases to 1 (because n1->other still holds it)
    // Since neither count reached 0, NEITHER Node is destroyed! Memory Leak!
    return 0;
} // Output will NOT show Node destructor messages!

flowchart LR
    BN1["shared_ptr bn1"]
    BN2["shared_ptr bn2"]

    BN1 -->|strong_other = bn2| BN2
    BN2 -.->|other = weak_ptr to bn1| BN1

    style BN2 fill:#dfd
    style BN1 fill:#dfd

5. std::weak_ptr: Breaking Cycles

  • Concept: A non-owning “weak” reference to an object managed by shared_ptr. It allows you to observe an object without affecting its lifetime (it doesn’t change the reference count).
  • Purpose: Primarily used to break reference cycles involving shared_ptr.
  • Usage:
    • Create a weak_ptr from a shared_ptr.
    • Cannot access the object directly. You must try to “lock” it.
    • lock(): Attempts to create a temporary shared_ptr from the weak_ptr. If the original object still exists, it returns a valid shared_ptr (incrementing the count while the temporary shared_ptr exists). If the object has already been deleted, it returns an empty (null) shared_ptr.
    • expired(): Checks if the object the weak_ptr used to point to has been deleted.

Fixing the Cycle with weak_ptr:

C++
#include <iostream>
#include <memory>

struct BetterNode {
    int id;
    std::weak_ptr<BetterNode> other; // Use weak_ptr to avoid cycle!
    std::shared_ptr<BetterNode> strong_other; // Maybe keep one strong link if needed

    BetterNode(int i) : id(i) { std::cout << "BetterNode " << id << " Created\n"; }
    ~BetterNode() { std::cout << "BetterNode " << id << " Destroyed\n"; }

    void checkOther() {
        std::cout << "Checking other node from " << id << ": ";
        // Try to get temporary ownership
        if (std::shared_ptr<BetterNode> shared_other = other.lock()) {
             std::cout << "Other is Node " << shared_other->id << "\n";
             // Can safely use shared_other here
        } else {
            std::cout << "Other node no longer exists.\n";
        }
    }
};

int main() {
    std::shared_ptr<BetterNode> bn1 = std::make_shared<BetterNode>(11); // bn1 count = 1
    std::shared_ptr<BetterNode> bn2 = std::make_shared<BetterNode>(22); // bn2 count = 1

    std::cout << "Linking nodes (one way strong, one way weak)...\n";
    bn1->strong_other = bn2; // bn2 count = 2 (Strong link)
    bn2->other = bn1;        // bn1 count = 1 (Weak link - no change)

    std::cout << "bn1 use_count: " << bn1.use_count() << std::endl; // Output: 1
    std::cout << "bn2 use_count: " << bn2.use_count() << std::endl; // Output: 2

    bn1->checkOther(); // Will report other node doesn't exist (nullptr)
    bn2->checkOther(); // Will report Node 11

    std::cout << "Leaving main scope...\n";
    // 1. bn1 goes out of scope. Its count is 1, drops to 0. BetterNode 11 is destroyed.
    // 2. bn2 goes out of scope. Its count was 2 (from main + bn1->strong_other).
    //    Destruction of BetterNode 11 destroys bn1->strong_other, reducing bn2's count to 1.
    //    bn2 going out of scope reduces its count to 0. BetterNode 22 is destroyed.
    // No leak!
    return 0;
} // Output WILL show both destructor messages

Summary of Lesson 11:

  • Smart pointers (unique_ptr, shared_ptr, weak_ptr) automate dynamic memory management using RAII.
  • std::unique_ptr: Exclusive ownership, lightweight, transfer via std::move. Use when one owner is sufficient.
  • std::shared_ptr: Shared ownership, uses reference counting. Use std::make_shared for creation. Object deleted when count is zero. Use when multiple owners need to keep an object alive.
  • shared_ptr cycles lead to memory leaks.
  • std::weak_ptr: Non-owning observer for objects managed by shared_ptr. Doesn’t affect reference count. Use lock() to safely get temporary access. Used to break shared_ptr cycles.
  • Choose the smart pointer that matches the required ownership semantics. Start with unique_ptr if possible.

Next Lesson: We’ll explore Move Semantics, Rvalue References, and std::move to understand how C++ achieves efficient resource transfers, especially for classes managing resources like smart pointers or containers.

More Sources:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top