رفتن به محتوا

shared_ptr، weak_ptr و گراف‌های مالکیت

مالکیت اشتراکی مبتنی بر شمارش ارجاع با shared_ptr را درک کنید، چرخه‌ها را با weak_ptr بشکنید، درباره بلوک‌های کنترل و نام‌گذاری مستعار بیاموزید و بدانید چه زمانی shared_ptr انتخاب نادرستی است.

shared_ptr: مالکیت اشتراکی

std::shared_ptr مالکیت اشتراکی را ممکن می‌سازد — چندین نمونه shared_ptr می‌توانند مالک یک شیء واحد باشند. شیء زمانی نابود می‌شود که آخرین shared_ptr مالک نابود شود یا بازنشانی (reset) شود.

در داخل، shared_ptr یک بلوک کنترل شامل موارد زیر نگهداری می‌کند:
- یک شمارنده ارجاع قوی (تعداد مالکان shared_ptr)
- یک شمارنده ارجاع ضعیف (تعداد ناظران weak_ptr)
- حذف‌کننده و تخصیص‌دهنده

shared_ptr بزرگ‌تر از اشاره‌گر خام است (معمولاً ۲ اشاره‌گر: یکی به شیء، یکی به بلوک کنترل) و شمارش ارجاع سربار اتمی در هر کپی/نابودسازی دارد. فقط وقتی مالکیت واقعاً اشتراکی است از آن استفاده کنید.

make_shared و شمارش ارجاع

std::make_shared(args...) شیء و بلوک کنترل را در یک تخصیص واحد تخصیص می‌دهد (به جای دو). این سازگارتر با کش است و مشکل ایمنی استثنا new خام را حل می‌کند.

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

weak_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 به یکدیگر داشته باشند، هیچ‌کدام نمی‌توانند به شمارش ارجاع صفر برسند — آن‌ها یکدیگر را برای همیشه زنده نگه می‌دارند. این یک نشت حافظه است، حتی با وجود استفاده از اشاره‌گرهای هوشمند.

struct Node {
    std::shared_ptr<Node> next;  // اگر next به عقب اشاره کند، نشت!
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a;  // دایره‌ای — نه a و نه b هرگز نابود نخواهند شد

راه‌حل: یک جهت را weak_ptr کنید. در ساختارهای درختی، فرزندان weak_ptr به والدین نگه می‌دارند (یا اشاره‌گرهای ناظر خام). در لیست‌های پیوندی دوطرفه، از weak_ptr برای اشاره‌گر prev استفاده کنید.

سازنده نام‌گذاری مستعار و enable_shared_from_this

سازنده نام‌گذاری مستعار (aliasing constructor) یک 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);  // بلوک کنترل p را به اشتراک می‌گذارد
// p می‌تواند از دامنه خارج شود — Pair زنده می‌ماند زیرا sp_first هنوز ارجاع دارد

enable_shared_from_this یک کلاس پایه CRTP است که به شیء مدیریت‌شده توسط shared_ptr اجازه می‌دهد از طریق shared_from_this() یک shared_ptr به خودش به دست آورد. بدون آن، ساختن shared_ptr جدید از this یک بلوک کنترل دوم ایجاد می‌کند که منجر به آزادسازی مضاعف می‌شود.

بهترین شیوه

shared_ptr بیش از حد استفاده می‌شود. فقط وقتی مالکیت واقعاً بین طول عمرهای مستقل مشترک است به سراغش بروید. از shared_ptr به عنوان جایگزین تنبلانه برای فکر کردن درباره مالکیت استفاده نکنید.

از shared_ptr اجتناب کنید وقتی:
- یک مالک واحد وجود دارد — به جای آن از unique_ptr استفاده کنید (سربار صفر).
- شیء روی پشته زندگی می‌کند — فقط مستقیماً از شیء استفاده کنید.
- فقط نیاز به ارسال ارجاع دارید — T& یا T* ارسال کنید.
- «چون امن است» استفاده می‌کنید — shared_ptr هزینه زمان اجرا دارد (شمارش ارجاع اتمی، تخصیص اضافی) و معناشناسی مالکیت را پنهان می‌کند.
- در مسیرهای حساس به عملکرد — عملیات شمارش ارجاع اتمی تحت تضاد (contention) به‌طور شگفت‌آوری گران هستند.

سناریوهای مشروع مالکیت اشتراکی: کش‌ها (چندین جستجو همان شیء را برمی‌گردانند)، سیستم‌های pub/sub، اشیاء پیکربندی مشترک و ساختارهای داده گرافی با گره‌های واقعاً مشترک.

جزئیات داخلی بلوک کنترل

درک بلوک کنترل به توضیح هزینه‌های 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. چگونه به‌صورت ایمن از طریق `weak_ptr` به شیء دسترسی پیدا می‌کنید؟