RAII: نمط الموارد الأساسية
تعرف على RAII (الحصول على الموارد هو التهيئة) - نمط C++ الأساسي لإدارة الموارد الآمنة للاستثناء. قم بتطبيق أغلفة RAII، وفهم lock_guard وscoped_lock، وتعرف على سبب ضرورة نقل أنواع RAII فقط أو إحصاء المرجع.
RAII: الحصول على المُنشئ، والإفراج عنه في المدمر
RAII (الاستحواذ على الموارد هو التهيئة) هو النمط الأكثر أهمية في C++. الفكرة بسيطة:
1. الحصول على مورد في المنشئ (افتح ملفًا، وقم بقفل كائن المزامنة (mutex)، وتخصيص الذاكرة، والاتصال بقاعدة بيانات).
2. حرر المورد الموجود في destructor (أغلق الملف، وقم بإلغاء قفل كائن المزامنة، وحرر الذاكرة، واقطع الاتصال).
نظرًا لأن C++ يضمن استدعاء أدوات التدمير عندما يخرج الكائن عن النطاق — حتى لو تم طرح استثناء — فإن RAII يجعل إدارة الموارد تلقائية وآمنة للاستثناءات. لا تنسى أبدًا أن تطلق سراحك، ولا تطلق سراحك مرتين أبدًا.
يستخدم كل نوع C++ مصمم جيدًا والذي يدير أحد الموارد RAII:std::string،std::vector،std::unique_ptr،std::fstream،std::lock_guard،std::unique_lock.
تنفيذ غلاف RAII
فيما يلي غلاف RAII كامل لواصف الملف (POSIX). فهو يكتسب في المنشئ، ويطلق في المدمر، ويدعم دلالات النقل لنقل الملكية.
#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، تتطلب التعليمات البرمجية الآمنة للاستثناء كتل محاولة/التقاط مملة لكل مورد. مع RAII، يكون الأمر تلقائيًا — حيث يتم تشغيل كل أداة تدمير أثناء تفكيك المكدس.
#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، وSscoped_lock
توفر المكتبة القياسية مغلفات RAII لكائنات المزامنة:
- std::lock_guard — أبسط قفل RAII. يُقفل في المنشئ، ويفتح في المدمر. غير منقولة وغير قابلة للنسخ. استخدمه عندما تحتاج إلى قفل بسيط النطاق.
- std::unique_lock — قفل RAII مرن. يدعم القفل المؤجل، والقفل الموقوت، والفتح/إعادة القفل اليدوي، ويمكن تحريكه. مطلوب للاستخدام معstd::condition_variable.
- std::scoped_lock (C++17) — يقفل العديد من عمليات المزامنة في وقت واحد باستخدام خوارزمية تجنب حالة الجمود. هذا هو نوع القفل المفضل عند قفل واحد أو أكثر من كائنات المزامنة.
std::mutex m1, m2;
// C++17: lock both without deadlock risk
std::scoped_lock lock(m1, m2);1. يتم تدمير كائنات RAII غير المسماة على الفور:
std::lock_guard<std::mutex>(mtx); // BUG! Temporary — locks and immediately unlocks
std::lock_guard<std::mutex> lock(mtx); // Correct — named, lives until scope end2. Never naively copy RAII types: Copying a file handle or mutex lock makes no sense. RAII types should be:
- Move-only (like unique_ptr, unique_lock) for exclusive ownership, or
- Reference-counted (like shared_ptr) for shared ownership.
3. Destructor should not throw: If a destructor throws during stack unwinding (another exception is active), std::terminate is called. Destructors are noexcept بشكل افتراضي في C++11+.
1. يجب أن يكون كل مورد مملوكًا لكائن RAII — الذاكرة، والملفات، والأقفال، والمقابس، واتصالات قاعدة البيانات، ومقابض وحدة معالجة الرسومات.
2. مورد واحد لكل فئة RAII — حافظ على تركيز الأغلفة.
3. حذف عمليات النسخ ما لم يكن نسخ المورد مفيدًا.
4. تنفيذ عمليات النقل لتمكين نقل الملكية.
5. استخدمstd::exchangeفي عمليات النقل - فهو يقرأ المصدر ويعيد تعيينه ذريًا:fd_ = std::exchange(other.fd_, -1);
6. تفضل أنواع RAII القياسية (unique_ptr،lock_guard،fstream) — اكتب فقط الأغلفة المخصصة للموارد التي لا تحتوي على أغلفة قياسية.
تأليف أنواع RAII (قاعدة الصفر)
التعبير النهائي عن RAII هو قاعدة الصفر: إذا كانت جميع متغيرات الأعضاء لديك من أنواع RAII، فإن صفك لا يحتاج إلى عدم وجود أعضاء خاصين معلنين من قبل المستخدم. تقوم عمليات التدمير والنسخ والنقل التي ينشئها المترجم بالشيء الصحيح تلقائيًا.
class DatabaseSession {
std::unique_ptr<Connection> conn_; // RAII: auto-closed
std::unique_lock<std::mutex> lock_; // RAII: auto-unlocked
std::vector<std::string> log_; // RAII: auto-freed
// No destructor, copy, or move needed — all member types handle it
};قاعدة الصفر هي الهدف. القاعدة الخمسة (التي تحدد بشكل صريح جميع الأعضاء الخمسة الخاصين) مخصصة لأنواع مجمّع الموارد ذات المستوى المنخفض. ليس هناك حل وسط – إذا أعلنت عن أي من الخمسة، أعلن عنهم جميعا.
- RAII: الاكتساب في المنشئ، والتحرير في المدمر - تلقائي وآمن للاستثناءات
- يتم استدعاء أدوات التدمير أثناء تفكيك المكدس، بحيث يتعامل RAII مع الاستثناءات بشكل طبيعي
- يجب أن تكون أنواع RAII منقولة فقط (ملكية حصرية) أو مُعدة كمرجع (ملكية مشتركة)
- استخدم
std::scoped_lock(C++17) لقفل متعدد المزامنة بدون حالة توقف تام - يتم تدمير كائنات RAII غير المسماة على الفور — قم دومًا بتسميتها
- اتبع قاعدة الصفر: استخدم أعضاء RAII حتى يقوم المترجم بإنشاء أعضاء خاصين صحيحين
اختبار — اختبر معرفتك
(15 نقطة خبرة)1. ما هو الخطأ في`std::lock_guard<std::mutex>(mtx);`(ملاحظة: لا يوجد اسم متغير)؟
2. لماذا يجب على أنواع RAII عادةً حذف عمليات النسخ الخاصة بها؟
3. ما هي قاعدة الصفر؟