C++ / المتقدم / الدرس 15
سلامة الموارد عبر حدود الملكية
كيّف الملكية الخارجية مع RAII، وحدّد قواعد الحذف والمراقبة، واحمِ أعمار الموارد عند خروج ردود النداء من نطاق إنشائها.
في هذه الصفحة
أهداف الدرس
- استخدام دالة حذف مخصصة بسياسة تحرير مطابقة صريحة.
- إبقاء حالة الحاذف والموارد المستعارة حية مدة كافية.
- الاختيار بين الاحتفاظ بالملكية والمراقبة الضعيفة لردود النداء.
مطابقة الاكتساب والتحرير بدقة
قد تعيد واجهات خارجية مقابض تحتاج دالة تحرير خاصة بدل delete. غلّف المقبض بنوع RAII صغير أو مؤشر ذكي بحاذف مطابق فور نجاح الاكتساب. تحقق هل يعني null الفشل، وهل يفشل التحرير، وهل تفرض الواجهة قيود خيط أو ترتيب. لا تمزج malloc مع delete ولا new مع free. يصح الغلاف فقط إذا حفظ عقد الواجهة الأصلية؛ فوضع عنوان خام في مؤشر ذكي لا يحدد دلالة التنظيف الصحيحة وحده.
إظهار اعتمادات التنظيف
يستخدم المثال الأول موردًا اصطناعيًا على الكومة وحاذفًا ذا حالة يزيد عدّاد تحرير خارجيًا. يُصرّح بالعداد قبل المالك فيعيش أطول منه. يستقبل منشئ unique_ptr التخصيص مباشرة، ولا ينفّذ الحاذف إلا زيادة صحيحة محصورة وحذفًا. ينبغي ألا ترمي أغلفة الموارد الفعلية أثناء التنظيف. يشكّل الحاذف ذو الحالة جزءًا من المالك، فتحتاج اعتماداته المستعارة إلى مراجعة أيضًا. يطابق المثال new مع delete عمدًا؛ ويحتاج مقبض C حقيقي إلى دالة تحريره الخاصة.
#include <iostream>
#include <memory>
struct Resource { int id; };
struct Release {
int* closed;
void operator()(Resource* resource) const noexcept {
++*closed;
delete resource;
}
};
int main() {
int closed{};
{
std::unique_ptr<Resource, Release> owner(new Resource{7}, Release{&closed});
std::cout << owner->id << "\n";
}
std::cout << closed << "\n";
}
7
1
يُقرأ المورد أثناء ملكيته ويُحرر مرة عند مغادرة النطاق. يبقى closed حيًا حتى ينتهي الحاذف.
العمل المخزن يمدد الاعتمادات
يعيش رد نداء مخزن للتنفيذ لاحقًا أطول من الاستدعاء الذي سجله. ينقل التقاط unique_ptr ملكية حصرية إلى إغلاق قابل للنقل فقط، فلا يُخزن في std::function الخاصة بـC++20 حين تتطلب النسخ. يبقي التقاط shared_ptr المورد حيًا حتى تزول ردود النداء المالكة. يسمح weak_ptr بانتهاء العمر ويلزم بفحص lock عند التنفيذ. هذه سلوكيات منتج مختلفة وليست تحسينات متبادلة. قرر هل يحرر الإلغاء الموارد فورًا أو يسمح للعمل المنتظر بالاحتفاظ بها، ووثّق عمر الطابور وإتلافه.
تثبيت المراقب الضعيف طوال العملية
يعيد مثال المراقب optional<int> فيميّز القيمة الحية من انتهاء العمر دون وصول متدلٍ. تنشئ lock مالكًا مشتركًا مؤقتًا يبقى طوال القراءة. يترك فحص expired ثم فك مؤشر خام فجوة قد يختفي فيها مالك آخر. يرى الاستدعاء الأول قيمة حية، وبعد مغادرة المالك الصريح يبقى الإغلاق لكن التقاطه الضعيف لا يحتفظ بالعدد. المثال بخيط واحد، ونمط الملكية نفسه مفيد عند التنفيذ غير المتزامن.
#include <functional>
#include <iostream>
#include <memory>
#include <optional>
int main() {
std::function<std::optional<int>()> observe;
{
auto owner = std::make_shared<int>(42);
observe = [weak = std::weak_ptr<int>{owner}]() -> std::optional<int> {
if (auto locked = weak.lock()) { return *locked; }
return std::nullopt;
};
std::cout << observe().value() << "\n";
}
std::cout << (observe().has_value() ? "live" : "expired") << "\n";
}
42
expired
لا يملك الالتقاط الضعيف العدد. يحمي المالك المؤقت المقفل القراءة، ويبلغ الاستدعاء اللاحق عن انتهاء العمر.
مراجعة كل مسارات الإكمال
تعالج الشيفرة الآمنة الإكمال العادي والعودة المبكرة والاستثناء والإلغاء والإنشاء الجزئي. إذا رمى منشئ تُدمّر الأعضاء المكتملة ولا يعمل مدمّر الكائن المحيط. ضع كل مورد مكتسب في عضو مالك سريعًا ليبقى الإنشاء الجزئي آمنًا. للعمليات متعددة الموارد حدّد ما يمكن التراجع عنه وما يحتاج تعويضًا. اختبر عدّ التحرير بمقابض اصطناعية ثم استخدم تشخيص الذاكرة والخيوط على تشغيل واقعي. وافحص مخارج المؤشرات الخام؛ فتقرير خالٍ من التسرب لا يثبت صلاحية كل وصول مستعار.
الأخطاء الشائعة وإصلاحها
حاذف يصل إلى عدّاد ميت.
- لماذا يحدث؟
- ماتت حالته المستعارة قبل المقبض المالك.
- كيف تصلحه؟
- أعد الترتيب أو امتلك حالة الحاذف لتعيش حتى التنظيف.
- كيف تتجنب تكراره؟
- أدرج اعتمادات الحاذف في مراجعة الملكية.
مراقب ضعيف يستخدم مؤشرًا خامًا قديمًا.
- لماذا يحدث؟
- فُصل فحص الانتهاء عن اكتساب الملكية.
- كيف تصلحه؟
- استخدم lock واحتفظ بـshared_ptr الناتج أثناء الوصول.
- كيف تتجنب تكراره؟
- عامل نتيجة lock كحارس للعمر.
مورد C يتعطل أثناء تنظيف مؤشر ذكي.
- لماذا يحدث؟
- لا يطابق الحاذف الافتراضي واجهة الاكتساب.
- كيف تصلحه؟
- وفّر عملية التحرير الموثقة كحاذف.
- كيف تتجنب تكراره؟
- وثّق الاكتساب والتحرير زوجًا لا ينفصل.
اختبر فهمك
1. غيّر التقاط المراقب من weak_ptr إلى shared_ptr. اشرح العمر بعد خروج المالك الأصلي وما يجب أن يحرر الملكية الأخيرة.
اعرض الحل المشروح
يملك الإغلاق المخزن العدد فيبقى حيًا بعد إتلاف المالك الأصلي. يحرر مسح observe أو إتلافه shared_ptr الملتقط، ويُدمّر العدد حينها إذا لم يبقَ مالك آخر. قد يناسب ذلك عقد العمل المحتفَظ به، لكنه لا يمثل مراقبة اختيارية لمورد يمكن أن ينتهي مستقلًا.
2. يفتح منشئ موردين ويرمي اكتساب الثاني. اشرح ترتيب أعضاء آمنًا وأي مدمّرات تعمل.
اعرض الحل المشروح
خزّن كل مورد في عضو RAII فور نجاح اكتسابه بترتيب الاعتماد. إذا فشل إنشاء العضو الثاني يُدمّر الأول المكتمل تلقائيًا. لم يكتمل إنشاء الكائن المحيط، فلا يُستدعى مدمّره. أما إبقاء الأول في متغيّر خام غير مدار فيصعّب ضمان التنظيف كثيرًا.
أهم ما تعلمته
- طابق سياسات التحرير وأبقِ اعتمادات الحاذف حية.
- ردود النداء المخزنة قرارات عمر.
- راجع الإنشاء الجزئي والإلغاء كمسارات عادية.