Menu
flag Ar iconالعربيةdown icon

الاستثناءات في C++: throw وwhat() ومعالجة الأخطاء

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

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

لماذا توجد الاستثناءات

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

الاستثناءات تحلّ هذا. عندما يحدث خطأ ما، تُطلق كائنًا باستخدام throw. يتوقف التنفيذ فورًا، ويُفرَّغ المكدّس (فيُشغَّل المُدمِّر لكل كائن محلي بين الإطلاق والمعالِج)، ويقفز التحكّم إلى أقرب catch مطابق. لا يمكن تجاهل استثناء غير معالَج بصمت: إن لم يلتقطه شيء، يستدعي البرنامج std::terminate ويُجهَض.

تركّز هذه الصفحة على جانب الإطلاق، أي كائنات الخطأ نفسها. أما الصفحة التالية فتتعمّق في آلية try/catch بالتفصيل.

الإطلاق ورسالة what()

تقنيًا يمكنك أن تُطلق (throw) أي قيمة —throw 42; أو throw "oops"; كلاهما قانوني— لكن لا تفعل ذلك. العُرف الذي يتبعه الجميع هو إطلاق كائن مشتق من std::exception. يعلن هذا الصنف الأساسي عن دالة افتراضية واحدة، what()، تُرجع وصفًا من نوع const char* للمشكلة. الالتزام بالعُرف يعني أن catch (const std::exception& e) واحدة تستطيع معالجة أي شيء.

تمنحك ترويسة <stdexcept> أنواعًا جاهزة يأخذ مُنشِئها الرسالة:

لاحظ أن what() تُرجع السلسلة النصية ذاتها التي أنشأت بها الاستثناء. ولاحظ أيضًا أننا التقطناه باستخدام const exception& رغم أننا أطلقنا runtime_error؛ وهذا ينجح لأن runtime_error هو std::exception (علاقة ستعرفها من صفحة الوراثة).

تسلسل الاستثناءات القياسي

قبل أن تكتب نوع استثناء خاصًا بك، تحقّق ممّا إذا كانت المكتبة القياسية تملك بالفعل نوعًا مناسبًا. جميعها ترث من std::exception وتنقسم إلى عائلتين في <stdexcept>:

  • logic_error — عيب في منطق البرنامج كان يمكن، مبدئيًا، اكتشافه قبل التشغيل. تشمل أنواعه الفرعية invalid_argument وout_of_range وdomain_error وlength_error.
  • runtime_error — فشل لا يظهر إلا أثناء التشغيل وليس خطأ برمجيًا بحدّ ذاته. تشمل أنواعه الفرعية range_error وoverflow_error وunderflow_error.

كثير من دوال المكتبة تُطلق هذه نيابةً عنك. على سبيل المثال، تُجري std::vector::at() فحصًا للحدود وتُطلق out_of_range بدلًا من السماح لك بالقراءة بعد النهاية:

هذه الدالة at() هي النظير الآمن لـ v[9]. أما operator[] العادي فلا يُجري أي فحص للحدود: قراءة v[9] هنا سلوك غير معرَّف، وليست استثناءً. اختيار at() هو الطريقة التي تحوّل بها فسادًا صامتًا إلى خطأ يمكن التقاطه.

اختر النوع الذي يصف الخطأ: invalid_argument عندما يمرّر المُستدعي شيئًا بلا معنى، وout_of_range لمشكلات الفهرس/المفتاح، وruntime_error لحالات "العالم الخارجي خذلني".

كتابة نوع استثناء خاص بك

عندما لا يناسبك أي نوع قياسي —تريد إرفاق بيانات إضافية، أو التقاط (catch) خطئك تحديدًا دون سواه— عرِّف صنفًا يرث من std::exception (أو من أحد أنواعه الفرعية) وأعِد تعريف what(). الوراثة من std::runtime_error هي أسهل طريق، لأنه يخزّن الرسالة بالفعل ويُنفّذ what() نيابةً عنك:

بما أن NetworkError يحمل رمز حالة، يستطيع المعالِج أن يتفاعل معه: يعيد المحاولة عند 5xx، ويستسلم عند 4xx. سلسلة خطأ مجرّدة لم تكن لتقدر على ذلك. كما يتيح النوع المخصّص لـ catch (const NetworkError&) أن يلتقط مشكلات الشبكة وحدها، ويترك كل ما عداها للمعالِج الأعمّ الذي يليه.

إذا ورِثت يومًا مباشرةً من std::exception (وليس من runtime_error)، فتذكّر أن تعيد تعريف what() بنفسك، وأن تضع علامة noexcept عليها لتطابق توقيع الصنف الأساسي:

class ParseError : public std::exception {
public:
    const char* what() const noexcept override {
        return "failed to parse input";
    }
};

أطلِق بالقيمة، والتقِط بالمرجع

هذه أهم قاعدة في استثناءات C++، وهي التي يخطئ فيها المبتدئون. أطلِق الكائنات بالقيمة، والتقِطها بمرجع const.

throw runtime_error("oops");            // بالقيمة - صحيح
catch (const runtime_error& e) { ... }  // بمرجع const - صحيح

أما الالتقاط بالقيمة بدلًا من ذلك —catch (std::exception e)— فينسخ الاستثناء إلى كائن من الصنف الأساسي و_يقتطع_ الجزء المشتق. بعد الاقتطاع، تستدعي e.what() تنفيذ الصنف الأساسي، لا تنفيذك المُعاد تعريفه، فتختفي رسالتك التي صغتها بعناية:

try {
    throw NetworkError(503, "service unavailable");
} catch (std::exception e) {       // بالقيمة - اقتطاع الكائن!
    std::cout << e.what();         // رسالة عامة، وقد اختفت status()
}

المرجع (&) يحفظ النوع الديناميكي الحقيقي، فتُوزَّع what() الافتراضية توزيعًا صحيحًا، ويظل بإمكانك الوصول إلى أعضاء الصنف المشتق. أضِف const لأنك تقرأ الاستثناء فقط ولا تعدّله. لا تُطلق مؤشرًا أبدًا (throw new runtime_error(...)): إذ سيتعيّن على المُلتقِط أن يستدعي delete عليه، وفي أي مسار تنفيذ؟ هذا بالضبط هو التسريب الذي يُفترض بالاستثناءات أن تمنعه.

التالي: try-catch

تستطيع الآن إنشاء استثناءات سليمة الصياغة وإطلاقها بـ throw، واختيار النوع القياسي المناسب لكل حالة فشل. النصف الآخر من القصة هو جانب الالتقاط. تغطّي الصفحة التالية try/catch بالكامل: ترتيب كتل catch المتعددة من الأكثر تخصيصًا إلى الأكثر عمومية، والكتلة الجامعة catch (...)، وإعادة الإطلاق بـ throw; المجرّدة، وكيف يضمن RAII (تذكّر المؤشرات الذكية) تحرير مواردك أثناء تفريغ المكدّس.

الأسئلة الشائعة

ما هو الاستثناء في C++؟

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

ما الفرق بين throw وreturn للأخطاء؟

قيمة return يجب أن يفحصها المُستدعي، ومن السهل نسيان ذلك، فيمضي البرنامج ببساطة ببيانات فاسدة. أما الاستثناء المُطلَق فلا يمكن تجاهله: إن لم يلتقطه أحد، أُنهي البرنامج. الاستثناءات للأعطال الحقيقية (ملف لا يُفتح، أو مُدخَل غير صالح)؛ بينما تظل قيم الإرجاع هي الصحيحة للنتائج العادية، بما في ذلك حالات "غير موجود" المتوقّعة.

ماذا تفعل دالة what() في استثناءات C++؟

كل صنف مشتق من std::exception يوفّر دالة افتراضية what() تُرجع const char* يصف الخطأ. عندما تلتقط استثناءً، يمنحك استدعاء e.what() الرسالة القابلة للقراءة البشرية التي يمكنك تسجيلها أو طباعتها. وأنواع الاستثناءات القياسية تضبطها من السلسلة النصية التي تمرّرها إلى مُنشِئها.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن