Skip to content

👁️ weak_ptr — Breaking Cycles

std::weak_ptrobserver — không sở hữu object nhưng có thể kiểm tra nó còn tồn tại hay không.

Analogy: Người quan sát

┌─────────────────────────────────────────────────────────────────┐
│                    OBSERVER ANALOGY                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   shared_ptr = Chủ sở hữu căn nhà                              │
│   weak_ptr = Người quan sát (không sở hữu)                     │
│                                                                 │
│   • Quan sát có thể nhìn nhà (nếu còn tồn tại)                 │
│   • Quan sát KHÔNG giữ nhà lại                                  │
│   • Khi tất cả chủ sở hữu bỏ đi → Nhà bị phá                   │
│   • Quan sát phải kiểm tra: "Nhà còn không?" trước khi vào     │
│                                                                 │
│   ┌─────────┐         ┌──────────────────┐                     │
│   │ Owner   │────────►│ Shared Resource  │                     │
│   └─────────┘         │  ref_count: 1    │                     │
│                       │  weak_count: 2   │                     │
│   ┌─────────┐         └──────────────────┘                     │
│   │Observer │- - - - - - - - ↗ (non-owning)                   │
│   │(weak_ptr)│                                                  │
│   └─────────┘                                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

The Circular Reference Problem

cpp
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "Node destroyed\n"; }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    
    a->next = b;  // a → b
    b->next = a;  // b → a (CYCLE!)
    
    std::cout << "a.use_count(): " << a.use_count() << "\n";  // 2
    std::cout << "b.use_count(): " << b.use_count() << "\n";  // 2
    
}  // Exit: No "Node destroyed" printed! LEAK!
     CIRCULAR REFERENCE
     ──────────────────
     
         a                b
     ┌───────┐        ┌───────┐
     │  ──────────────►      │
     │       │        │       │
     │       ◄──────────────  │
     └───────┘        └───────┘
     use_count=2      use_count=2
     
     When main exits:
     a goes out of scope → a.use_count = 1 (b still points to a)
     b goes out of scope → b.use_count = 1 (a still points to b)
     
     Neither reaches 0 → MEMORY LEAK!

Solution: weak_ptr

cpp
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // ← weak_ptr breaks cycle!
    
    ~Node() { std::cout << "Node destroyed\n"; }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    
    a->next = b;    // a → b (shared)
    b->prev = a;    // b ← a (weak, doesn't count!)
    
    std::cout << "a.use_count(): " << a.use_count() << "\n";  // 1
    std::cout << "b.use_count(): " << b.use_count() << "\n";  // 2
    
}  // Both nodes destroyed correctly!

Output:

a.use_count(): 1
b.use_count(): 2
Node destroyed
Node destroyed

Basic Usage

cpp
#include <memory>

int main() {
    std::weak_ptr<int> weak;
    
    {
        auto shared = std::make_shared<int>(42);
        weak = shared;  // weak observes shared
        
        std::cout << "expired: " << weak.expired() << "\n";  // 0 (false)
        
        // To use weak_ptr, must lock() to get shared_ptr
        if (auto sp = weak.lock()) {
            std::cout << *sp << "\n";  // 42
        }
        
    }  // shared destroyed
    
    std::cout << "expired: " << weak.expired() << "\n";  // 1 (true)
    
    if (auto sp = weak.lock()) {
        // This branch won't execute
    } else {
        std::cout << "Object is gone\n";
    }
}

lock() Method

lock() returns a shared_ptr:

  • If object exists: returns valid shared_ptr
  • If object destroyed: returns empty shared_ptr
cpp
std::weak_ptr<Widget> cache;

void useCache() {
    // Try to get shared_ptr from weak_ptr
    if (std::shared_ptr<Widget> sp = cache.lock()) {
        // Object still alive, use it
        sp->doSomething();
    } else {
        // Object was deleted, need to recreate
        auto newWidget = std::make_shared<Widget>();
        cache = newWidget;
        newWidget->doSomething();
    }
}

Real Example: Tree Structure

cpp
#include <memory>
#include <vector>

struct TreeNode {
    int value;
    std::weak_ptr<TreeNode> parent;  // Parent = weak (prevent cycle)
    std::vector<std::shared_ptr<TreeNode>> children;  // Children = shared
    
    explicit TreeNode(int v) : value(v) {}
};

std::shared_ptr<TreeNode> createTree() {
    auto root = std::make_shared<TreeNode>(1);
    
    auto child1 = std::make_shared<TreeNode>(2);
    auto child2 = std::make_shared<TreeNode>(3);
    
    child1->parent = root;  // weak reference
    child2->parent = root;
    
    root->children.push_back(child1);  // shared reference
    root->children.push_back(child2);
    
    return root;
}

// When root destroyed, all children destroyed too
// No memory leak!

Real Example: Observer Pattern

cpp
#include <memory>
#include <vector>
#include <algorithm>

class Subject {
    std::vector<std::weak_ptr<Observer>> observers_;
    
public:
    void addObserver(std::shared_ptr<Observer> obs) {
        observers_.push_back(obs);  // Store as weak
    }
    
    void notify() {
        // Clean up expired observers and notify active ones
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [](const std::weak_ptr<Observer>& wp) {
                    return wp.expired();
                }),
            observers_.end()
        );
        
        for (auto& wp : observers_) {
            if (auto sp = wp.lock()) {
                sp->onNotify();
            }
        }
    }
};

Real Example: Cache

cpp
#include <memory>
#include <unordered_map>

class ResourceCache {
    std::unordered_map<std::string, std::weak_ptr<Resource>> cache_;
    
public:
    std::shared_ptr<Resource> get(const std::string& key) {
        // Try to get from cache
        auto it = cache_.find(key);
        if (it != cache_.end()) {
            if (auto sp = it->second.lock()) {
                return sp;  // Cache hit!
            }
            // Resource was deleted, remove from cache
            cache_.erase(it);
        }
        
        // Cache miss - load resource
        auto resource = loadResource(key);
        cache_[key] = resource;  // Store as weak
        return resource;
    }
};

When to Use weak_ptr

ScenarioUse
Breaking cyclesParent-child, doubly-linked
CachingResource pools, memoization
Observer patternNon-owning subscribers
Optional referenceMay or may not exist

📚 Tổng kết

FeatureDescription
OwnershipNone (observer only)
Extends lifetime❌ No
Access objectMust call lock()
Check validityexpired() or lock()
Main useBreaking cycles

➡️ Tiếp theo

Khi tạo class quản lý resource, cần biết Rule of 5:

Rule of 5 → — Special member functions.