انتقل إلى المحتوى

shared_ptr, weak_ptr & Ownership Graphs

افهم الملكية المشتركة المُعدة حسب المرجع باستخدام Shared_ptr، وقم بفصل الدورات باستخدام Weak_ptr، وتعرف على كتل التحكم والاسم المستعار، واعرف متى يكون Shared_ptr هو الاختيار الخاطئ.

shared_ptr: Shared Ownership

std::shared_ptrenables shared ownership — multipleshared_ptrinstances can own the same object. The object is destroyed when the last owningshared_ptris destroyed or reset.

Internally,shared_ptrmaintains a control block containing:
- A strong reference count (number ofshared_ptrowners)
- A weak reference count (number ofweak_ptrobservers)
- The deleter and allocator

shared_ptris larger than a raw pointer (typically 2 pointers: one to the object, one to the control block) and reference counting has atomic overhead on every copy/destruction. Use it only when ownership is genuinely shared.

make_shared والعد المرجعي

std::make_shared(args...)allocates the object AND control block in a single allocation (instead of two). This is more cache-friendly and avoids the exception-safety issue of rawnew.

shared_ptr_basics.cpp
#include <iostream>
#include <memory>
#include <string>

struct Logger {
    std::string name;
    Logger(std::string n) : name(std::move(n)) {
        std::cout << name << " created\n";
    }
    ~Logger() { std::cout << name << " destroyed\n"; }
    void log(const std::string& msg) {
        std::cout << "[" << name << "] " << msg << '\n';
    }
};

int main() {
    auto logger = std::make_shared<Logger>("AppLogger");
    std::cout << "use_count: " << logger.use_count() << '\n';  // 1

    {
        auto copy = logger;  // shared ownership
        std::cout << "use_count: " << logger.use_count() << '\n';  // 2
        copy->log("inside scope");
    }  // copy destroyed, count drops to 1

    std::cout << "use_count: " << logger.use_count() << '\n';  // 1
    logger->log("still alive");
}  // logger destroyed — count drops to 0, Logger deleted

الضعيف_ptr: كسر الدورات

يراقبstd::weak_ptrكائنًا يُدار بواسطةshared_ptr دون إطالة عمره . للوصول إلى الكائن، اتصل بـlock()، الذي يُرجعshared_ptr— أوnullptrإذا تم تدمير الكائن بالفعل. يعدweak_ptrضروريًا لكسر المراجع الدائرية التي قد تتسرب.

weak_ptr_example.cpp
#include <iostream>
#include <memory>
#include <string>

struct Employee;
struct Team {
    std::string name;
    std::vector<std::shared_ptr<Employee>> members;
    Team(std::string n) : name(std::move(n)) {}
    ~Team() { std::cout << "Team " << name << " destroyed\n"; }
};

struct Employee {
    std::string name;
    std::weak_ptr<Team> team;  // weak — does NOT own the team
    Employee(std::string n) : name(std::move(n)) {}
    ~Employee() { std::cout << "Employee " << name << " destroyed\n"; }

    void print_team() {
        if (auto t = team.lock()) {  // try to get shared_ptr
            std::cout << name << " belongs to " << t->name << '\n';
        } else {
            std::cout << name << " has no team\n";
        }
    }
};

int main() {
    auto team = std::make_shared<Team>("Engineering");
    auto alice = std::make_shared<Employee>("Alice");
    auto bob = std::make_shared<Employee>("Bob");

    team->members.push_back(alice);
    team->members.push_back(bob);
    alice->team = team;  // weak_ptr — no circular ownership
    bob->team = team;

    alice->print_team();  // "Alice belongs to Engineering"

    team.reset();  // destroy team
    alice->print_team();  // "Alice has no team" — weak_ptr expired
}
خطأ شائع

إذا كان هناك كائنان يربطانshared_ptrببعضهما البعض، فلن يتمكن أي منهما من الوصول إلى الرقم المرجعي صفر - فهم يبقيان بعضهما البعض على قيد الحياة إلى الأبد. يعد هذا تسربًا للذاكرة، على الرغم من أنك تستخدم مؤشرات ذكية.

المؤشر ``cpp
struct Node {
std::shared_ptr next; // if next points back, LEAK!
};
auto a = std::make_shared();
auto b = std::make_shared();
a->next = b;
b->next = a; // circular — neither a nor b will ever be destroyed
``

The fix: make one direction a weak_ptr. In tree structures, children hold weak_ptr to parents (or raw observer pointers). In doubly-linked lists, use weak_ptr for the prev.

منشئ الاسم المستعار وenable_shared_from_this

يقوم المنشئ المستعار بإنشاءshared_ptrالذي يشترك في الملكية معshared_ptrموجود ولكنه يشير إلى كائن مختلف (عادةً ما يكون عضوًا). يبقى الكائن ذو الاسم المستعار حيًا طالما يوجد أي اسم مستعار لـshared_ptr:

struct Pair { int first; int second; };
auto p = std::make_shared<Pair>(Pair{1, 2});
std::shared_ptr<int> sp_first(p, &p->first);  // shares p's control block
// p can go out of scope — Pair stays alive because sp_first still references it

enable_shared_from_this is a CRTP base class that allows an object managed by shared_ptr to obtain a shared_ptr to itself via shared_from_this(). Without it, constructing a new shared_ptr from this ينشئ كتلة تحكم ثانية، مما يؤدي إلى المضاعفة الحرة.

أفضل ممارسة

تم الإفراط في استخدامshared_ptr. لا يمكنك الوصول إليه إلا عندما تتم مشاركة الملكية بشكل حقيقي عبر فترات حياة مستقلة. لا تستخدمshared_ptrكبديل كسول للتفكير في الملكية.

تجنبshared_ptrعندما:
- يوجد مالك واحد — استخدمunique_ptrبدلاً من ذلك (بدون حمل).
- الكائن موجود في المكدس — فقط استخدم الكائن مباشرة.
- تحتاج فقط إلى تمرير مرجع — قم بتمريرT&أوT*.
- أنت تستخدمه "لأنه آمن" — يحتويshared_ptrعلى تكلفة وقت التشغيل (إعادة العد الذري، والتخصيص الإضافي) ويخفي دلالات الملكية.
- في المسارات الحرجة للأداء — تكون عمليات إعادة العد الذري باهظة الثمن بشكل مدهش في ظل التنافس.

سيناريوهات الملكية المشتركة المشروعة: ذاكرات التخزين المؤقت (عمليات البحث المتعددة تعرض نفس الكائن)، وأنظمة النشر/الفرعية، وكائنات التكوين المشتركة، وهياكل بيانات الرسم البياني مع العقد المشتركة بالفعل.

التحكم في كتلة الداخلية

يساعد فهم كتلة التحكم في تفسير تكاليفshared_ptr. يجمعmake_sharedبين الكائن وكتلة التحكم في تخصيص واحد. يتطلب البناء منnewالخام تخصيصين منفصلين.

control_block.cpp
#include <memory>
#include <iostream>

struct Widget {
    int data[4];
};

int main() {
    // Two allocations: one for Widget, one for control block
    std::shared_ptr<Widget> p1(new Widget{});

    // Single allocation: Widget + control block together
    auto p2 = std::make_shared<Widget>();

    // Sizes (typical 64-bit platform)
    std::cout << "sizeof(Widget*):          " << sizeof(Widget*) << '\n';   // 8
    std::cout << "sizeof(unique_ptr<Widget>): " << sizeof(std::unique_ptr<Widget>) << '\n';  // 8
    std::cout << "sizeof(shared_ptr<Widget>): " << sizeof(std::shared_ptr<Widget>) << '\n';  // 16

    // shared_ptr is 2 pointers: one to object, one to control block
    // Control block contains: strong count, weak count, deleter, allocator

    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // 1
    std::weak_ptr<Widget> w = p2;
    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // still 1
    // weak_ptr does NOT increase strong count
}
النقاط الرئيسية
  • يستخدمshared_ptrالعد المرجعي - يموت الكائن عند رحيل المالك الأخير
  • يخصصmake_sharedالكائن + كتلة التحكم في تخصيص واحد (يفضل ذلك دائمًا)
  • weak_ptrيراقب دون امتلاك — استخدمlock()للوصول بأمان وكسر المراجع الدائرية
  • shared_ptrيبلغ عرضه مؤشرين ويحتوي على إعادة حساب ذرية - وليس مجانيًا
  • الافتراضي هوunique_ptr؛ استخدمshared_ptrفقط عندما تكون الملكية مشتركة بشكل حقيقي
  • لا تقم أبدًا بإنشاء اثنين منshared_ptrمن نفس المؤشر الأولي - استخدمenable_shared_from_thisللمراجع الذاتية

اختبار — اختبر معرفتك

(15 نقطة خبرة)

1. ماذا يحدث عندما يربط جسمان`shared_ptr`ببعضهما البعض؟

2. لماذا يُفضل`std::make_shared`على`shared_ptr<T>(new T(...))`؟

3. How do you safely access an object through a`weak_ptr`?