مسیر
/
C++
C++
/
برنامه‌ی درسی
/
اشاره‌گرهای هوشمند
اش

اشاره‌گرهای هوشمند در C++

1 تمرین

درباره‌ی اشاره‌گرهای هوشمند

«اشاره‌گرهای هوشمند» ویژگی‌ای مدرن در C++ هستند که برای ارائه‌ی مدیریت خودکار حافظه طراحی شده‌اند و به جلوگیری از نشت حافظه و اشاره‌گرهای معلق، که معمولاً همراه اشاره‌گرهای خام پیش می‌آیند، کمک می‌کنند. آن‌ها مثل پوششی دور اشاره‌گرهای خام عمل می‌کنند و قابلیت‌های بیشتری به آن اضافه می‌کنند، مثلاً آزادسازی خودکار حافظه وقتی دیگر به اشاره‌گر نیازی نیست.

نحوه‌ی نگارش کلی

اشاره‌گرهای هوشمند معمولاً به‌صورت قالب‌های کلاس در کتابخانه‌ی استاندارد C++ پیاده‌سازی می‌شوند. دو اشاره‌گر هوشمند پرکاربردتر std::unique_ptr و std::shared_ptr هستند.

اشاره‌گرهای یکتا

std::unique_ptr اشاره‌گری هوشمند است که شیء را به‌صورت انحصاری در مالکیت خود دارد. این اشاره‌گر تضمین می‌کند که در هر لحظه، تنها یک شیء std::unique_ptr مالک منبع باشد. وقتی std::unique_ptr مالک از بین برود یا بازنشانی شود، به‌طور خودکار اشیاء را از بین می‌برد و حافظه‌شان را آزاد می‌کند.

#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");

// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr

مزایای std::make_unique()

وقتی می‌خواهید یک std::unique_ptr بسازید، بهتر است به‌جای استفاده‌ی مستقیم از new برای تخصیص حافظه، از std::make_unique() استفاده کنید. std::make_unique() چندین مزیت دارد:

  1. ایمنی در برابر استثنا: std::make_unique() ایمنی در برابر استثنا را تضمین می‌کند. اگر هنگام ساخت شیء استثنایی پرتاب شود، حافظه به‌طور خودکار آزاد می‌شود و از نشت حافظه جلوگیری می‌شود.
  2. شفافیت: استفاده از std::make_unique() کد را شفاف‌تر و موجزتر می‌کند. دیگر نیازی نیست نوعی را که تخصیص داده می‌شود صریحاً مشخص کنید، چون آرگومان‌های قالب به‌طور خودکار استنتاج می‌شوند.
  3. فرصت‌های بهینه‌سازی: کامپایلرها می‌توانند std::make_unique() را مؤثرتر از تخصیص دستی حافظه با new بهینه‌سازی کنند و این می‌تواند در نهایت به عملکرد بهتر منجر شود.
  4. پرهیز از استفاده‌ی نادرست: اگر std::unique_ptr را به‌صورت دستی بسازید، حذف منبع زیربنایی ممکن می‌شود. آن‌گاه وقتی std::unique_ptr بخواهد در پایان محدوده‌ی خود آن را حذف کند، این کار به رفتار تعریف‌نشده منجر می‌شود.

اشاره‌گرهای اشتراکی

std::shared_ptr اشاره‌گری هوشمند است که به چندین شیء std::shared_ptr اجازه می‌دهد مالکیت یک منبع یکسان را با هم داشته باشند. این اشاره‌گر پی می‌گیرد که چند اشاره‌گر اشتراکی به منبع ارجاع می‌دهند و حافظه را تنها زمانی آزاد می‌کند که آخرین اشاره‌گر اشتراکی مالک منبع از محدوده خارج شود یا بازنشانی شود.

// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");

// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
Caution

در C++17 و نسخه‌های پیش از آن، استفاده از std::shared_ptr همراه با آرایه‌ها از طریق std::make_shared<T[]> به‌طور مستقیم پشتیبانی نمی‌شود. هرچند تخصیص آرایه‌ها با std::make_shared<T[]> ممکن است، ساختن اشاره‌گرهای اشتراکی مستقیماً از آن‌ها می‌تواند به دلیل تفاوت‌های مدیریت حافظه میان اشیاء منفرد و آرایه‌ها به رفتار تعریف‌نشده منجر شود. در عوض، بهتر است برای مدیریت مؤثر آرایه‌ها همراه با اشاره‌گرهای اشتراکی از std::vector یا توابع حذف سفارشی استفاده کنید. همیشه هنگام کار با تخصیص آرایه‌ها و اشاره‌گرهای اشتراکی در C++17 سازگاری با کامپایلر و پیاده‌سازی کتابخانه‌ی استاندارد خود را تضمین کنید.

مزایای std::make_shared()

std::make_shared() همانند std::make_unique() مزایایی مانند بهره‌وری بیشتر حافظه، ایمنی در برابر استثنا و خوانایی ارائه می‌دهد. این تابع تخصیص حافظه برای بلوک کنترل و شیء مدیریت‌شده را در یک عملیات واحد ترکیب می‌کند و به این ترتیب کارایی را بالا می‌برد و خطر نشت حافظه را کاهش می‌دهد. به‌علاوه، استنتاج خودکار آرگومان‌های قالب کد را ساده‌تر و خواناتر می‌کند. استفاده از std::make_shared() هنگام کار با اشیاء std::shared_ptr در C++ کدی تمیزتر، ایمن‌تر و کارآمدتر به همراه دارد.

اشاره‌گرهای ضعیف

std::weak_ptr کلاسی همراه برای std::shared_ptr است که ارجاعی «ضعیف» و بدون مالکیت به شیئی که یک اشاره‌گر اشتراکی مدیریت می‌کند ارائه می‌دهد. این اشاره‌گر اجازه‌ی دسترسی به منبع را بدون تأثیر گذاشتن بر طول عمر آن می‌دهد. اشاره‌گرهای ضعیف در موقعیت‌هایی مفیدند که باید ارجاع‌های چرخه‌ای برای جلوگیری از نشت حافظه شکسته شوند.

// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;

// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.

std::weak_ptr و مالکیت چرخه‌ای

std::weak_ptr برای حل مسئله‌ی مالکیت چرخه‌ای، که با نام ارجاع‌های چرخه‌ای هم شناخته می‌شود، طراحی شد؛ مشکلی که ممکن است هنگام استفاده از std::shared_ptr پیش بیاید.

در سناریوی مالکیت چرخه‌ای، دو یا چند شیء std::shared_ptr به یکدیگر ارجاع می‌دهند و چرخه‌ای می‌سازند که در آن هیچ‌یک از اشیاء حذف نمی‌شود، چون ارجاع‌های قوی به یکدیگر دارند و همین به نشت حافظه می‌انجامد.

std::weak_ptr با اجازه دادن به ارجاع‌های ضعیف به اشیاء اشتراکی، بدون این‌که به شمارش ارجاع آن‌ها چیزی اضافه کند، راه‌حلی برای این مشکل ارائه می‌دهد. یعنی می‌تواند شیء اشتراکی را ببیند و به آن دسترسی پیدا کند، اما مانع حذف آن نمی‌شود. اگر همه‌ی ارجاع‌های قوی به شیء اشتراکی آزاد شوند، شیء از بین می‌رود و اشاره‌گرهای ضعیفی که آن را می‌پاییدند به‌طور خودکار به nullptr بازنشانی می‌شوند.

در ادامه مثالی کوتاه می‌آید که استفاده از std::weak_ptr را برای شکستن مالکیت چرخه‌ای نشان می‌دهد:

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::weak_ptr<Node> next;
};

// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;

// Release strong references to nodes
node1.reset();
node2.reset();

// Nodes are automatically destructed due to weak references

«Node constructed» دو بار چاپ می‌شود و پس از آن دو خط «Node destructed» می‌آید.

وقتی next یک اشاره‌گر ضعیف نباشد بلکه std::shared_ptr باشد، وضعیت مالکیت چرخه‌ای ایجاد می‌شود و به نشت حافظه می‌انجامد، چون هر دو گره ارجاع‌های قوی به یکدیگر نگه می‌دارند و همین مانع از بین رفتن آن‌ها می‌شود.

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::shared_ptr<Node> next; // Change to shared_ptr
};

در این سناریو، node1 و node2 هر دو از طریق std::shared_ptr ارجاع‌های قوی به یکدیگر نگه می‌دارند و چرخه‌ای تشکیل می‌دهند. وقتی برنامه پایان می‌یابد و می‌خواهد ارجاع‌های قوی را با reset() آزاد کند، شمارش ارجاع هر دو گره غیرصفر می‌ماند، چون هر گره ارجاع قوی به دیگری دارد. بنابراین، گره‌ها از بین نمی‌روند و همین به نشت حافظه می‌انجامد. «Node constructed» دو بار چاپ می‌شود، اما هیچ خطی از «Node destructed» پس از آن نمی‌آید.

اشاره‌گرهای معلق و std::weak_ptr

اشاره‌گرهای معلق وقتی پیش می‌آیند که یک اشاره‌گر به شیئی ارجاع دهد که حذف شده است؛ در این حالت، هنگام ارجاع‌زدایی از اشاره‌گر رفتار تعریف‌نشده رخ می‌دهد. std::weak_ptr با فراهم کردن ارجاعی ضعیف و بدون مالکیت به شیئی که std::shared_ptr مدیریت می‌کند، به پرهیز از اشاره‌گرهای معلق کمک می‌کند. در ادامه می‌بینید چطور با std::weak_ptr از اشاره‌گرهای معلق پرهیز کنید:

  1. یک std::weak_ptr بسازید: به‌جای این‌که مستقیماً یک std::shared_ptr نگه دارید، یک std::weak_ptr به شیء اشتراکی بسازید.

  2. اعتبار آن را بررسی کنید: پیش از استفاده از std::weak_ptr، اعتبار آن را با تابع expired() بررسی کنید. این تابع اگر شیء اشتراکی مرتبط حذف شده باشد true برمی‌گرداند.

  3. std::weak_ptr را قفل کنید: برای دسترسی ایمن به شیء اشتراکی، از تابع lock() استفاده کنید که اگر شیء هنوز معتبر باشد، یک std::shared_ptr اشاره‌کننده به همان شیء برمی‌گرداند. اگر شیء حذف شده باشد، lock() یک std::shared_ptr خالی برمی‌گرداند.

در ادامه نمونه‌کدی می‌آید که استفاده از std::weak_ptr را برای پرهیز از اشاره‌گرهای معلق نشان می‌دهد:

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Create weak pointers
node1->next = node2;
node2->next = node1;

// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
    // Use lockedNode2 safely
} else {
    // Handle case where node2 has been deleted
}

در این مثال، node1->next و node2->next اشیاء std::weak_ptr هستند. پیش از دسترسی به شیء اشتراکی، اعتبار آن با تابع expired() بررسی می‌شود. سپس از lock() برای دسترسی ایمن به شیء اشتراکی استفاده می‌شود. این روش تضمین می‌کند که هنگام کار با std::weak_ptr از اشاره‌گرهای معلق پرهیز شود.

توصیه‌های استفاده

به‌طور پیش‌فرض از اشاره‌گرهای هوشمند استفاده کنید: std::unique_ptr برای مالکیت انحصاری و std::shared_ptr برای مالکیت اشتراکی. اشاره‌گرهای خام را برای ارجاع‌های بدون مالکیت یا برای ارتباط با کدهای قدیمی نگه دارید. در بیشتر موارد، std::unique_ptr برای مالکیت انحصاری کافی است، چون مدیریت حافظه‌ی سبکی بدون سربار شمارش ارجاع فراهم می‌کند. از std::shared_ptr باید کم استفاده کرد، چون تا زمانی که مالکیت اشتراکی واقعی لازم نباشد، سربار و پیچیدگی به همراه می‌آورد. std::weak_ptr برای شکستن وابستگی‌های چرخه‌ای یا مشاهده‌ی اشیاء اشتراکی تخصص یافته است، اما کاربرد رایجی ندارد.

ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

اشاره‌گرهای هوشمند را یاد بگیرید