رفتن به محتوا

RAII: الگوی اساسی مدیریت منابع

RAII (تحصیل منبع همان مقداردهی اولیه است) را بیاموزید — الگوی اصلی ‎C++‎ برای مدیریت منابع ایمن در برابر استثنا. پوشش‌دهنده‌های RAII را پیاده‌سازی کنید، lock_guard و scoped_lock را درک کنید و بیاموزید چرا انواع RAII باید فقط قابل انتقال یا شمارش‌شده با ارجاع باشند.

RAII: تحصیل در سازنده، آزادسازی در مخرب

RAII (تحصیل منبع همان مقداردهی اولیه است — Resource Acquisition Is Initialization) مهم‌ترین الگو در ‎C++‎ است. ایده ساده است:

1. یک منبع را در سازنده تحصیل کنید (باز کردن فایل، قفل کردن mutex، تخصیص حافظه، اتصال به پایگاه داده).
2. منبع را در مخرب آزاد کنید (بستن فایل، باز کردن قفل mutex، آزادسازی حافظه، قطع اتصال).

چون ‎C++‎ تضمین می‌کند که مخرب‌ها هنگام خروج شیء از دامنه فراخوانی می‌شوند — حتی اگر استثنایی پرتاب شود — RAII مدیریت منابع را خودکار و ایمن در برابر استثنا می‌کند. هرگز فراموش نمی‌کنید آزاد کنید و هرگز دوبار آزاد نمی‌کنید.

هر نوع ‎C++‎ خوب‌طراحی‌شده که منبعی مدیریت می‌کند از RAII استفاده می‌کند: std::string، std::vector، std::unique_ptr، std::fstream، std::lock_guard، std::unique_lock.

پیاده‌سازی پوشش‌دهنده RAII

اینجا یک پوشش‌دهنده RAII کامل برای توصیف‌گر فایل (POSIX) آمده است. در سازنده تحصیل می‌کند، در مخرب آزاد می‌کند و از معناشناسی انتقال برای انتقال مالکیت پشتیبانی می‌کند.

raii_file_descriptor.cpp
#include <unistd.h>   // open, close, read, write
#include <fcntl.h>    // O_RDONLY, etc.
#include <stdexcept>
#include <utility>
#include <iostream>

class FileDescriptor {
    int fd_ = -1;  // -1 = no resource
public:
    // Acquire: open file in constructor
    explicit FileDescriptor(const char* path, int flags)
        : fd_(::open(path, flags)) {
        if (fd_ < 0) throw std::runtime_error("Cannot open file");
    }

    // Release: close file in destructor
    ~FileDescriptor() {
        if (fd_ >= 0) ::close(fd_);
    }

    // Non-copyable (copying a file descriptor is meaningless)
    FileDescriptor(const FileDescriptor&) = delete;
    FileDescriptor& operator=(const FileDescriptor&) = delete;

    // Moveable (transfer ownership)
    FileDescriptor(FileDescriptor&& other) noexcept
        : fd_(std::exchange(other.fd_, -1)) {}

    FileDescriptor& operator=(FileDescriptor&& other) noexcept {
        if (this != &other) {
            if (fd_ >= 0) ::close(fd_);
            fd_ = std::exchange(other.fd_, -1);
        }
        return *this;
    }

    int get() const { return fd_; }
};

int main() {
    try {
        FileDescriptor fd("/tmp/raii_test.txt", O_RDONLY);
        std::cout << "File opened: fd=" << fd.get() << '\n';
        // Use fd...
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }
    // fd.~FileDescriptor() called automatically — file closed
    // Even if an exception was thrown, the file is closed
}

ایمنی استثنا از طریق RAII

بدون RAII، کد ایمن در برابر استثنا نیاز به بلوک‌های try/catch خسته‌کننده برای هر منبع دارد. با RAII، خودکار است — هر مخرب در طی باز کردن پشته اجرا می‌شود.

exception_safety.cpp
#include <memory>
#include <mutex>
#include <fstream>
#include <vector>
#include <stdexcept>

std::mutex g_mutex;

// WITHOUT RAII — manual cleanup is error-prone
void bad_example() {
    std::mutex* m = &g_mutex;
    m->lock();
    int* data = new int[100];
    // If this throws, mutex stays locked AND data leaks!
    // process(data);
    delete[] data;
    m->unlock();
}

// WITH RAII — automatic, exception-safe cleanup
void good_example() {
    std::lock_guard<std::mutex> lock(g_mutex);  // RAII lock
    auto data = std::make_unique<int[]>(100);   // RAII allocation
    // If this throws:
    //   1. data's destructor frees the memory
    //   2. lock's destructor unlocks the mutex
    //   Stack unwinding handles everything
    // process(data.get());
}

// Multiple resources — RAII composes naturally
void multi_resource() {
    std::lock_guard<std::mutex> lock(g_mutex);
    auto buffer = std::make_unique<char[]>(4096);
    std::ofstream file("/tmp/output.txt");

    // If any operation throws, ALL resources are released
    // in reverse order of construction:
    //   1. file closed
    //   2. buffer freed
    //   3. mutex unlocked
}

lock_guard، unique_lock و scoped_lock

کتابخانه استاندارد پوشش‌دهنده‌های RAII برای mutex‌ها فراهم می‌کند:

- std::lock_guard — ساده‌ترین قفل RAII. در سازنده قفل می‌کند، در مخرب باز می‌کند. غیرقابل انتقال، غیرقابل کپی. وقتی به قفل ساده دامنه‌ای نیاز دارید استفاده کنید.

- std::unique_lock — قفل RAII انعطاف‌پذیر. از قفل تأخیری، قفل زمان‌دار، باز/قفل دستی پشتیبانی می‌کند و قابل انتقال است. برای استفاده با std::condition_variable الزامی است.

- std::scoped_lock (‎C++‎17) — چندین mutex را همزمان با الگوریتم اجتناب از بن‌بست قفل می‌کند. این نوع قفل ترجیحی هنگام قفل یک یا چند mutex است.

std::mutex m1, m2;
// ‎C++‎17: هر دو را بدون خطر بن‌بست قفل کنید
std::scoped_lock lock(m1, m2);
دام

۱. اشیاء RAII بدون نام بلافاصله نابود می‌شوند:

std::lock_guard<std::mutex>(mtx);  // باگ! موقتی — قفل کرده و فوراً باز می‌کند
std::lock_guard<std::mutex> lock(mtx);  // صحیح — نام‌دار، تا پایان دامنه زنده می‌ماند

۲. هرگز انواع RAII را ساده‌لوحانه کپی نکنید: کپی کردن دسته فایل یا قفل mutex معنایی ندارد. انواع RAII باید:
- فقط قابل انتقال باشند (مانند unique_ptr، unique_lock) برای مالکیت انحصاری، یا
- شمارش‌شده با ارجاع باشند (مانند shared_ptr) برای مالکیت اشتراکی.

۳. مخرب نباید استثنا پرتاب کند: اگر مخرب هنگام باز کردن پشته (استثنای دیگری فعال است) استثنا پرتاب کند، std::terminate فراخوانی می‌شود. مخرب‌ها به‌صورت پیش‌فرض در ‎C++‎11 و بعد noexcept هستند.

بهترین شیوه

1. هر منبع باید متعلق به یک شیء RAII باشد — حافظه، فایل‌ها، قفل‌ها، سوکت‌ها، اتصالات پایگاه داده، دسته‌های GPU.
2. یک منبع به ازای هر کلاس RAII — پوشش‌دهنده‌ها را متمرکز نگه دارید.
3. عملیات کپی را حذف کنید مگر اینکه کپی منبع معنادار باشد.
4. عملیات انتقال را پیاده‌سازی کنید تا انتقال مالکیت ممکن شود.
5. از std::exchange در عملیات انتقال استفاده کنید — به‌صورت اتمی می‌خواند و منبع را بازنشانی می‌کند: fd_ = std::exchange(other.fd_, -1);
6. انواع RAII استاندارد را ترجیح دهید (unique_ptr، lock_guard، fstream) — فقط برای منابع بدون پوشش‌دهنده استاندارد پوشش‌دهنده سفارشی بنویسید.

ترکیب انواع RAII (قاعده صفر)

بالاترین بیان RAII قاعده صفر (Rule of Zero) است: اگر همه متغیرهای عضو شما انواع RAII باشند، کلاس شما به هیچ عضو ویژه اعلان‌شده توسط کاربر نیاز ندارد. مخرب، کپی و عملیات انتقال تولیدشده توسط کامپایلر به‌صورت خودکار کار درست را انجام می‌دهند.

class DatabaseSession {
    std::unique_ptr<Connection> conn_;    // RAII: خودکار بسته می‌شود
    std::unique_lock<std::mutex> lock_;   // RAII: خودکار باز می‌شود
    std::vector<std::string> log_;        // RAII: خودکار آزاد می‌شود
    // نیازی به مخرب، کپی یا انتقال نیست — همه انواع عضو آن را مدیریت می‌کنند
};

قاعده صفر هدف است. قاعده پنج (تعریف صریح هر ۵ عضو ویژه) برای انواع پوشش‌دهنده منابع سطح پایین است. حد وسطی وجود ندارد — اگر هر یک از پنج را اعلان می‌کنید، همه را اعلان کنید.

نکات کلیدی
  • RAII: تحصیل در سازنده، آزادسازی در مخرب — خودکار و ایمن در برابر استثنا
  • مخرب‌ها در طی باز کردن پشته فراخوانی می‌شوند، بنابراین RAII استثناها را به‌صورت طبیعی مدیریت می‌کند
  • انواع RAII باید فقط قابل انتقال (مالکیت انحصاری) یا شمارش‌شده با ارجاع (مالکیت اشتراکی) باشند
  • از std::scoped_lock (‎C++‎17) برای قفل چند mutex بدون بن‌بست استفاده کنید
  • اشیاء RAII بدون نام بلافاصله نابود می‌شوند — همیشه نامی به آن‌ها بدهید
  • از قاعده صفر پیروی کنید: از اعضای RAII استفاده کنید تا کامپایلر اعضای ویژه صحیح را تولید کند

آزمون — دانش خود را بسنجید

(15 امتیاز)

1. مشکل `std::lock_guard<std::mutex>(mtx);` (توجه: بدون نام متغیر) چیست؟

2. چرا انواع RAII معمولاً باید عملیات کپی خود را حذف کنند؟

3. قاعده صفر چیست؟