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 شیء و بلوک کنترل را در یک تخصیص واحد تخصیص میدهد (به جای دو). این سازگارتر با کش است و مشکل ایمنی استثنا new خام را حل میکند.
#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 deletedweak_ptr: شکستن چرخهها
std::weak_ptr یک شیء مدیریتشده توسط shared_ptr را بدون تمدید طول عمرش مشاهده میکند. برای دسترسی به شیء، lock() را فراخوانی کنید که یک shared_ptr برمیگرداند — یا nullptr اگر شیء قبلاً نابود شده باشد. weak_ptr برای شکستن ارجاعهای دایرهای که در غیر این صورت نشت میکنند ضروری است.
#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 خام نیاز به دو تخصیص جداگانه دارد.
#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` به شیء دسترسی پیدا میکنید؟