اشاره‌گرهای هوشمند: unique_ptr

اشاره گر های هوشمند اومدن تا کار مارو راحت کنن. تا دیگه کمتر ذهن برنامه نویس درگیر امنیت اشاره گر ها و مسائل دیگه باشه.

موضوع اشاره گر ها همیشه یکی از چالش های زبان هایی مثل سی و سی++ بوده.اینکه یه حافظه ای رو از سیستم بگیریم، آزادش کنیم و مراقب باشیم که حافظهای که گرفته شده معتبر باشه تا برای استفاده کردن ازش یا آزاد کردنش مشکلیپیش نیاد و هزارتا چیز دیگه.

اشاره گر های هوشمند اومدن تا کار مارو راحت کنن. تا دیگه کمتر ذهن برنامه نویس درگیر امنیت اشاره گر ها و مسائل دیگه باشه.

بنابراین یکی از ارکان سی++ مدرن میتونه استفاده از اشاره‌گر های هوشمند باشه.

توی این پست راجع به کلاس unique_ptr و make_unique می‌نویسم.

چرا از اشاره‌گر هوشمند استفاده کنیم

به این قطعه کد دقت کنید:

#include <iostream>
  
void someFunction()
{
    auto *ptr{ new Resource() };
  
    int x{};
    std::cout << "Enter an integer: ";
    std::cin >> x;
  
    if (x == 0)
        throw 0; // the function returns early, and ptr won’t be deleted!
  
    // do stuff with ptr here
  
    delete ptr;
}

مشکلی که هست اینه که اگر x = 0 اونوقت یک exception پرتاب میشه و همونجا کار تابع تموم میشه بنابراین برنامه هیچوقت به اون خطی که در اون delete نوشته شده نمیرسه و اینجا memory leak یا نشت حافظه رخ میده.

شاید بگیم خب این یک موضوع ساده‌ست و میشه قبل از پرتاب کردن استثنا، بیایم و حافظه رو پاک کنیم. خب این یه کار اضافه برای برنامه نویسه. سخته که برنامه نویس همش حواسش باشه که کجا حافظه گرفته و کجا آزادش نکرده.

اگر میشد حافظه پویایی که اشاره گر ها بهش اشاره می‌کنن هم مثل متغییر ها و اشیاء معمولی که وقتی out of scope میشن ازبین میرن، آزاد بشن خیلی خوب میشد.

ایده اشاره گر های هوشمند هم همینه. یک شئ از یک کلاس ساخته میشه که وظیفه‌ش نگهداری از حافظه گرفته شده‌ست. همونطور که می‌دونیم وقتی اشیاء out of scope می‌شن، ازبین میرن و تابع destructor اونها صدا زده میشه. پس فقط کافیه که این کلاس ما در دیستراکتور خودش بیاد و حافظه ای که گرفته شدهرو delete کنه.

استفاده از unique_ptr

تا قبل از سی++ ۱۱ کلاسی به اسم auto_ptr وجود داشت که خیلی مزخرف بود. سی++ ۱۱ با معرفی unique_ptr اومد و کلاس قبلی رو منسوخ کرد و کلاس auto_ptr توی سی++ ۱۷ به کلی حذف شد.

این کلاس اوپراتور های ستاره(*) و -> رو overload کرده بنابراین میشه تقریبا مثل پوینتر معمولی ازش استفاده کرد.

یک مثال ساده از unique_ptr :

#include <iostream>
#include <memory> // for std::unique_ptr
  
class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};
  
int main()
{
    // allocate a Resource object and have it owned by std::unique_ptr
    std::unique_ptr<Resource> res{ new Resource() };
  
    return 0;
} // res goes out of scope here, and the allocated Resource is destroyed

چون شئ ما روی استک ساخته میشه، وقتی که تابع به اتمام میرسه تمام متغییرها/اشیاء از بین میرن و به این ترتیب تابع دیستراکتور این کلاس میاد وحافظه ای که گرفته شده رو آزاد می‌کنه. بدون اینکه نیاز باشه ما نگرانی خاصی بابت آزاد شدنش داشته باشیم.

مالکیت اشاره گر در unique_ptr

این امکان موجود نیست که دو شئ از unique_ptr یک اشاره گر مشترک رو مدیریت کنند. به همین دلیل این کلاس توابع مربوط به کپی رو غیرفعال کرده.

بنابراین برای اینکه محتوای یک شئ از unique_ptr رو برابر با شئ دیگری از همین کلاس قرار بدیم باید دوتا نکته رو مدنظر داشته باشیم:

  • باید از move semantics استفاده بکنیم. بنابراین اگر مقدار ما r-value نیست باید از std::move استفاده بکنیم.
  • بعد از اینکار، شئ اولیه ما (که در سمت راست علامت مساوی قرار میگیره) دیگه مسئول مدیریت اون اشاره گر نیست و یک شئ خالی محسوب میشه.

مثال:

#include <iostream>
#include <memory> // for std::unique_ptr
  
class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
};
  
int main()
{
    std::unique_ptr<Resource> res1{ new Resource{} }; // Resource created here
    std::unique_ptr<Resource> res2{}; // Start as nullptr
  
    std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
    std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null\n" : "null\n");
  
    // res2 = res1; // Won't compile: copy assignment is disabled
    res2 = std::move(res1); // res2 assumes ownership, res1 is set to null
  
    std::cout << "Ownership transferred\n";
  
    std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
    std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null\n" : "null\n");
  
    return 0;
} // Resource destroyed here when res2 goes out of scope

نتیجه این کد به این صورته:

Resource acquired
res1 is not null
res2 is null
Ownership transferred
res1 is null
res2 is not null
Resource destroyed

unique_ptr و آرایه ها

خوشبختانه این کلاس میتونه تشخیص بده که چه زمانی باید ازdelete[] استفاده کنه و چه زمانی از delete و همچنین باهاش میشه آرایه هم ساخت.

std::make_unique

این کلاس که توی سی++ ۱۴ اضافه شده این امکان رو فراهم کرده که به سادگی با unique_ptr کار کنیم و همچنین از مشکلات احتمالی مثل exception safety و یا تغییر ناخواسته اشاره گر اصلی(که کلاس unique_ptr داره ازش نگهداری می‌کنه) جلوگیری می‌کنه.

فقط کافیه بهش اسم اون type ای که قراره براش حافظه بگیریم رو به عنوان آرگومان های تمپلیت بهش بدیم و همچنین آرگومان هایی که میخوایم موقع ساختن اون type بهش داده بشه رو به عنوان آرگومان های تابعی به این کلاس بدیم. فکر کنم توضیحم گنگ بود بنابراین بهتره که به مثال توجه کنیم:

#include <memory> // for std::unique_ptr and std::make_unique
#include <iostream>
  
class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };
  
public:
    Fraction(int numerator = 0, int denominator = 1) :
        m_numerator{ numerator }, m_denominator{ denominator }
    {
    }
  
    friend std::ostream& operator<<(std::ostream& out, const Fraction &f1)
    {
        out << f1.m_numerator << '/' << f1.m_denominator;
        return out;
    }
};
  
  
int main()
{
    // Create a single dynamically allocated Fraction with numerator 3 and denominator 5
    // We can also use automatic type deduction to good effect here
    auto f1{ std::make_unique<Fraction>(3, 5) };
    std::cout << *f1 << '\n';
  
    // Create a dynamically allocated array of Fractions of length 4
    auto f2{ std::make_unique<Fraction[]>(4) };
    std::cout << f2[0] << '\n';
  
    return 0;
}

خروجی کد هم:

3 / 5
0 / 1

درکل توصیه میشه که بجای اینکه مستقیما از unique_ptr استفاده بشه، از این کلاس استفاده بشه.

هم بهینه تره و هم امن تر.

پاس دادن unique_ptr به یک تابع

همونطور که بالاتر گفتم، این کلاس توابع مربوط به کپی رو غیرفعال کرده بنابراین با استفاده از move semantic ها، وقتی انتقال انجام میشه دیگه شئ اولیه مسئول مدیریت حافظه مربوطه نیست.

بنابراین اگر یک شئ از این کلاس رو با استفاده از move به یک تابع پاس بدیم، مالکیتش به شئ داخل تابع منتقل میشه و با تموم شدن کار اون تابع، حافظه ما هم (که در بیرون از تابع allocate شده) آزاد میشه.

برای جلوگیری از این مشکل بهتره که از تابع get() استفاده بکنیم.

مثال زیر:

#include <memory> // for std::unique_ptr
#include <iostream>
  
class Resource
{
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
  
    friend std::ostream& operator<<(std::ostream& out, const Resource &res)
    {
        out << "I am a resource\n";
        return out;
    }
};
  
// The function only uses the resource, so we'll accept a pointer to the resource, not a reference to the whole std::unique_ptr<Resource>
void useResource(Resource *res)
{
    if (res)
        std::cout << *res << '\n';
}
  
int main()
{
    auto ptr{ std::make_unique<Resource>() };
  
    useResource(ptr.get()); // note: get() used here to get a pointer to the Resource
  
    std::cout << "Ending program\n";
  
    return 0;
} // The Resource is destroyed here

و خروجی کد هم به این زیبایی هست:

Resource acquired
I am a resource
Ending program
Resource destroyed

پایان

در آخر بهتره که بجای اشاره گر خام از اشاره گر هوشمند استفاده بکنیم و همچنین نکات مربوط به اونها رو یادمون بمونه:

  • سعی کنیم از make_unique استفاده بکنیم
  • وقتی مدیریت یک اشاره گر رو به این کلاس می‌سپریم دیگه نباید اون اشاره گر رو به صورت دستی تغییر بدیم(آزاد کنیم یا هرچیز دیگه)
  • نباید دو یا چند شئ از این کلاس از یک حافظه یکسان نگهداری کنن