Skip to content

🤝 shared_ptr — Shared Ownership

std::shared_ptr cho phép nhiều owners cùng sở hữu một object. Object tự động bị hủy khi owner cuối cùng biến mất.

Analogy: Sổ tiết kiệm chung

┌─────────────────────────────────────────────────────────────────┐
│                    SHARED BANK ACCOUNT                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   shared_ptr = Sổ tiết kiệm chung của nhóm                     │
│                                                                 │
│   • Nhiều người đồng sở hữu cùng tài khoản                      │
│   • Ngân hàng đếm số người sở hữu (reference count)             │
│   • Mỗi người rời đi → count giảm                               │
│   • Người cuối cùng rời → Tài khoản đóng (delete)              │
│                                                                 │
│   ┌─────────┐                                                   │
│   │ Owner A │──┐                                                │
│   └─────────┘  │         ┌──────────────────┐                  │
│                ├────────►│ Shared Resource  │                  │
│   ┌─────────┐  │         │  ref_count: 3    │                  │
│   │ Owner B │──┤         └──────────────────┘                  │
│   └─────────┘  │                                                │
│                │                                                │
│   ┌─────────┐  │                                                │
│   │ Owner C │──┘                                                │
│   └─────────┘                                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Basic Usage

cpp
#include <memory>

// Create with make_shared (PREFERRED)
auto ptr = std::make_shared<int>(42);

// Copy creates another owner
auto ptr2 = ptr;  // Now 2 owners

// Check reference count
std::cout << ptr.use_count() << "\n";  // 2

// Both ptr and ptr2 point to same value
std::cout << *ptr << " " << *ptr2 << "\n";  // 42 42

Reference Counting

cpp
#include <memory>

void demonstrate() {
    auto p1 = std::make_shared<int>(42);
    std::cout << "Count: " << p1.use_count() << "\n";  // 1
    
    {
        auto p2 = p1;  // Copy
        std::cout << "Count: " << p1.use_count() << "\n";  // 2
        
        auto p3 = p1;  // Another copy
        std::cout << "Count: " << p1.use_count() << "\n";  // 3
        
    }  // p2, p3 destroyed
    
    std::cout << "Count: " << p1.use_count() << "\n";  // 1
    
}  // p1 destroyed → ref_count = 0 → object deleted

Control Block

┌─────────────────────────────────────────────────────────────────┐
│                    CONTROL BLOCK STRUCTURE                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   shared_ptr                    Control Block                   │
│   ┌──────────────┐             ┌─────────────────────┐         │
│   │ object_ptr ──┼──────────►  │ shared_count: 2     │         │
│   │ control_ptr ─┼──────┐      │ weak_count: 1       │         │
│   └──────────────┘      │      │ deleter             │         │
│                         └─────►│ allocator           │         │
│                               └─────────────────────┘         │
│                                        │                       │
│                                        ▼                       │
│                               ┌─────────────────────┐         │
│                               │    Managed Object   │         │
│                               └─────────────────────┘         │
│                                                                 │
│   Overhead: ~16-24 bytes for control block                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Creating shared_ptr

cpp
#include <memory>

// ✅ PREFERRED: make_shared (single allocation)
auto p1 = std::make_shared<Widget>(args...);

// ✅ OK: From unique_ptr
std::unique_ptr<Widget> up = std::make_unique<Widget>();
std::shared_ptr<Widget> sp = std::move(up);

// ⚠️ Less efficient: Constructor (two allocations)
std::shared_ptr<Widget> p2(new Widget());

// ❌ DANGER: Two shared_ptrs from same raw pointer
Widget* raw = new Widget();
std::shared_ptr<Widget> sp1(raw);
std::shared_ptr<Widget> sp2(raw);  // 💥 Double delete!

Sharing Across Containers

cpp
#include <memory>
#include <vector>

class Document {
public:
    std::string content;
};

int main() {
    auto doc = std::make_shared<Document>();
    doc->content = "Hello";
    
    std::vector<std::shared_ptr<Document>> editors;
    std::vector<std::shared_ptr<Document>> viewers;
    
    editors.push_back(doc);
    viewers.push_back(doc);
    viewers.push_back(doc);
    
    std::cout << doc.use_count() << "\n";  // 4
    
    // All share the same document
    editors[0]->content = "Modified";
    std::cout << viewers[1]->content << "\n";  // "Modified"
}

Thread Safety

cpp
// shared_ptr itself is thread-safe for:
// ✅ Reading use_count()
// ✅ Copying shared_ptr instances
// ✅ Assigning shared_ptr instances

// But the OBJECT it points to is NOT automatically thread-safe!

auto ptr = std::make_shared<int>(0);

// Thread 1
auto copy1 = ptr;  // ✅ Safe to copy

// Thread 2
auto copy2 = ptr;  // ✅ Safe to copy

// Thread 1 & 2 both modifying *ptr
*copy1 = 10;  // ❌ Race condition on the int!
*copy2 = 20;  // ❌ Need mutex for object access

enable_shared_from_this

Khi object cần tạo shared_ptr của chính nó:

cpp
#include <memory>

class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getShared() {
        // ✅ Correct way
        return shared_from_this();
        
        // ❌ WRONG - creates new control block!
        // return std::shared_ptr<Node>(this);
    }
};

int main() {
    auto node = std::make_shared<Node>();
    auto same = node->getShared();
    
    std::cout << node.use_count() << "\n";  // 2
}

Custom Deleter

cpp
#include <memory>

void closeFile(FILE* f) {
    if (f) fclose(f);
}

int main() {
    // shared_ptr with custom deleter
    std::shared_ptr<FILE> file(
        fopen("data.txt", "r"),
        closeFile
    );
    
    // Or with lambda
    std::shared_ptr<FILE> file2(
        fopen("data.txt", "r"),
        [](FILE* f) { if (f) fclose(f); }
    );
}

unique_ptr vs shared_ptr

Aspectunique_ptrshared_ptr
Owners1Many
Copy❌ No✅ Yes
Move✅ Yes✅ Yes
OverheadNoneControl block
Thread-safeN/ARef counting
Default choice✅ YesWhen needed

⚠️ Circular Reference Problem

cpp
struct Node {
    std::shared_ptr<Node> next;
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    
    a->next = b;  // a → b
    b->next = a;  // b → a (circular!)
    
    // When main exits:
    // a.use_count() = 1 (from b->next)
    // b.use_count() = 1 (from a->next)
    // Neither reaches 0 → MEMORY LEAK!
}

Solution: weak_ptr


📚 Tổng kết

FeatureDescription
OwnershipShared (multiple owners)
Copy✅ Increases ref count
Thread safetyRef counting is atomic
OverheadControl block (~16-24 bytes)
When to useWhen ownership must be shared

➡️ Tiếp theo

Giải quyết vấn đề circular reference:

weak_ptr → — Breaking cycles.

🧠 Quiz

Câu 1: Đoạn code sau, use_count() cuối cùng là bao nhiêu?

cpp
auto p1 = std::make_shared<int>(42);
auto p2 = p1;
auto p3 = p1;
{
    auto p4 = p2;
}
  • [ ] A) 4
  • [x] B) 3
  • [ ] C) 2
  • [ ] D) 1

💡 Giải thích: p1 tạo → count=1. p2 = p1 → count=2. p3 = p1 → count=3. p4 = p2 trong block → count=4. Khi block kết thúc, p4 bị hủy → count=3. Cuối cùng còn p1, p2, p3use_count() = 3.

Câu 2: Vấn đề circular reference với shared_ptr là gì?

cpp
struct Node {
    std::shared_ptr<Node> next;
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a;
  • [ ] A) Compile error vì circular dependency
  • [ ] B) Chỉ a bị leak, b được giải phóng bình thường
  • [x] C) Cả ab đều không bao giờ được giải phóng — memory leak
  • [ ] D) Runtime exception khi chạy

💡 Giải thích: a giữ b (count=2), b giữ a (count=2). Khi a, b ra scope, count giảm xuống 1 nhưng không bao giờ về 0 → cả hai không bao giờ bị delete → memory leak. Giải pháp: dùng std::weak_ptr cho một trong hai chiều để phá vòng.

Câu 3: Tại sao nên dùng std::make_shared<T>() thay vì std::shared_ptr<T>(new T())?

  • [x] A) make_shared cấp phát một block duy nhất cho cả object và control block, hiệu quả hơn
  • [ ] B) make_shared tự động giải phóng memory nhanh hơn
  • [ ] C) shared_ptr(new T()) không compile được từ C++17
  • [ ] D) make_shared cho phép custom deleter

💡 Giải thích: make_shared thực hiện một lần cấp phát cho cả object và control block (reference count), trong khi shared_ptr(new T()) cần hai lần cấp phát riêng biệt. Ngoài ra, make_shared còn exception-safe hơn trong các biểu thức phức tạp.