محتويات الدورة

C++ / المتوسط / الدرس 11

الاستثناءات وضمانات الحالة

استخدم الاستثناءات لمسارات الفشل الموثقة، واترك التنظيف لـRAII، وصمّم العمليات كي لا يفسد العمل الفاشل حالة الكائن.

وقت القراءة التقريبي: 4 دقيقةتاريخ مراجعة المحتوى
في هذه الصفحة

أهداف الدرس

  • رمي أنواع استثناء قياسية ذات معنى والتقاطها بمرجع ثابت.
  • تمييز الضمان الأساسي والقوي وعدم الرمي.
  • تطبيق التحضير ثم الالتزام وتجنب المدمّرات الرامية.

الفشل جزء من الواجهة

قد تفشل دالة بسبب مخالفة الإدخال لقواعد المجال أو فشل تخصيص أو تعذر عملية خارجية. قرر أي حالات الفشل نتائج عادية وأيها استثناءات. يناسب غياب سجل اختياري optional غالبًا، وقد يناسب استثناء طلب إنشاء يخرق خاصية ثابتة. تنتمي std::expected إلى C++23 ولا تُستخدم هنا كمكوّن من C++20. اذكر إمكان الرمي وما يبقى دون تغيير عنده. تنقل الاستثناءات التحكم إلى معالج مطابق مع إتلاف الكائنات التلقائية في النطاقات المغادرة.

الإبلاغ عن فشل محدد في المجال

ارمِ كائن الاستثناء بالقيمة والتقط عائلات الاستثناء القياسية بمرجع ثابت. قد يقطع الالتقاط بالقيمة جزءًا مشتقًا وينسخ بلا حاجة. ضع المعالجات الخاصة قبل العامة. يرفض المثال الأول عدد دفعات سالبًا قبل الضرب، ويطبع المعالج الرسالة التي قدّمتها شيفرتنا. لا يعتمد الإخراج على صياغة مكتبية خاصة بالتطبيق. وحُصر المجال الموجب الصغير أيضًا لمنع فيض الإشارة. تعود الحسابات الناجحة عاديًا دون دخول المعالج.

cpp حساب مفحوص برسالة محددة
#include <iostream>
#include <stdexcept>
int units(int batches) {
    if (batches < 0 || batches > 1000) {
        throw std::invalid_argument("batches out of range");
    }
    return batches * 5;
}
int main() {
    try { std::cout << units(-1) << "\n"; }
    catch (const std::invalid_argument& error) {
        std::cout << error.what() << "\n";
    }
}
الناتج المتوقع
batches out of range

يرمي الطلب غير الصالح قبل إنتاج عدد، ويطبع المعالج المطابق الرسالة المخصّصة نفسها.

تسمية ما يحفظه الفشل

يحافظ الضمان الأساسي على الخصائص الثابتة ويمنع تسرب الموارد مع احتمال تغير القيم. يجعل الضمان القوي الفشل يترك الحالة الملحوظة دون تغيير. ويعد ضمان عدم الرمي بألا يخرج استثناء، ولا يعني منع الإبلاغ عن الفشل بقناة أخرى. هذه خواص للعملية كاملة لا لعبارة واحدة. فقد تغيّر عملية رصيدًا ثم تلحق سجلًا يحتاج إلى تخصيص وتفشل بعد تغير الرصيد. تحمي RAII تنظيف الموارد لكنها لا تتراجع تلقائيًا عن حالة العمل.

إنجاز العمل الخطر قبل تعديل المالك

حضّر مرشحًا في تخزين محلي مملوك وافحصه ثم التزم به بعملية لا ترمي. يفحص المثال الثاني العنوان المرشح قبل مبادلته بالحالي. تدعم std::string بمخصّصها القياسي المبادلة غير الرامية المستخدمة هنا. قد يفشل نسخ الوسيط قبل الدخول، لكنه يترك current دون تغيير أيضًا. في تجميعة حقيقية لا تكون سلسلة مبادلات معاملة قوية إلا إذا لم تفشل أي خطوة التزام ولم يرَ مراقب حالة جزئية. وتحتاج آثار الشبكة أو الملفات الخارجية إلى تصميم استعادة منفصل.

cpp فحص مرشح ثم الالتزام بالمبادلة
#include <iostream>
#include <stdexcept>
#include <string>
void replace_title(std::string& current, std::string candidate) {
    if (candidate.empty()) { throw std::invalid_argument("empty title"); }
    current.swap(candidate);
}
int main() {
    std::string title{"Draft"};
    try { replace_title(title, ""); }
    catch (const std::invalid_argument&) { std::cout << "rejected\n"; }
    std::cout << title << "\n";
    replace_title(title, "Final");
    std::cout << title << "\n";
}
الناتج المتوقع
rejected
Draft
Final

يترك المرشح الفارغ Draft كما هو، ويلتزم المقبول بالقيمة Final ثم تُنظّف القيمة القديمة محليًا.

معالجة الفشل حيث تكون الاستعادة مفيدة

التقط الاستثناء حيث تستطيع إعادة المحاولة أو تحويله إلى نتيجة حدية موثقة أو إضافة سياق مفيد وإعادة الرمي عبر throw;. قد يخفي المعالج العام الصامت افتراضات مكسورة. ينبغي أن تحرر المدمّرات الموارد دون نشر فشل، خاصة أثناء فك المكدس. إذا احتمل فشل الإغلاق أو الالتزام الصريح فأظهره كعملية منفصلة قبل الإتلاف. افحص في اختبارات الاستثناء فئة الخطأ والحالة بعده. واختبر مسارات التخصيص عند الإمكان؛ فالتقاط std::exception في كل مكان لا يغني عن تحديد مالك سياسة الاستعادة.

الأخطاء الشائعة وإصلاحها

استثناء مشتق يفقد هويته الخاصة.

لماذا يحدث؟
التُقط ونسخ كقيمة من النوع الأساس.
كيف تصلحه؟
التقط بمرجع ثابت ورتّب المعالجات من الخاص.
كيف تتجنب تكراره؟
تجنب التقاط الاستثناءات المتعددة الأشكال بالقيمة.

عملية فاشلة تترك حالة محدثة جزئيًا.

لماذا يحدث؟
حدث التعديل قبل عمل لاحق قد يرمي.
كيف تصلحه؟
حضّر وافحص محليًا ثم التزم دون رمي.
كيف تتجنب تكراره؟
حدّد ضمان الاستثناء المقصود واختبره.

التنظيف ينهي العملية على نحو غير متوقع.

لماذا يحدث؟
سمح مدمّر بخروج استثناء أثناء فك المكدس.
كيف تصلحه؟
أبقِ الإتلاف غير رامٍ وأظهر الإكمال القابل للفشل منفصلًا.
كيف تتجنب تكراره؟
راجع اعتمادات المدمّرات والإبلاغ عن فشلها.

اختبر فهمك

1. أضف اختبارات units عند صفر وألف وسالب واحد وألف وواحد. اذكر النتائج والطلبات التي يجب أن ترمي.

اعرض الحل المشروح

تعيد units(0) صفرًا وunits(1000) خمسة آلاف. يرمي الطلبان خارج المجال invalid_argument برسالتنا. أقصى ضرب هنا قابل للتمثيل بأمان في تطبيقات int المطابقة. ينبغي أن يفشل الاختبار إذا عاد الطلب غير الصالح عاديًا، لا أن يطبع نتيجة غير متوقعة فقط.

2. تنقص دالة المخزون ثم تضيف سجلًا إلى vector. إذا رمت push_back، هل يمكن ادعاء الضمان القوي؟ اقترح تصحيحًا.

اعرض الحل المشروح

لا، فقد تغير المخزون مسبقًا. حضّر حالة مرشحة كاملة تشمل إضافة السجل في كائنات محلية مالكة. بعد نجاح العمل القابل للرمي استبدل الحالة القديمة بالتزام ثبت عدم رميه. أو وثّق الضمان الأساسي فقط إذا ناسب التطبيق، ولا تسمّ التحديث الجزئي معاملة كاملة.

أهم ما تعلمته

  • حدّد قناة الفشل وضمان الحالة.
  • تنظّف RAII الموارد وتحفظ المعاملات حالة العمل.
  • التقط بالمرجع وأبقِ المدمّرات غير رامِية.

المراجع الرسمية