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

‏try-catch في C++: التعامل مع الاستثناءات بالطريقة الصحيحة

‏لُف الكود المحفوف بالمخاطر داخل try، وتفاعل في catch. تعلّم كيف تلتقط الاستثناءات عبر مرجع ثابت، وترتّب عدة معالِجات، وتستخدم catch (...)، وتعيد الإطلاق، دون تسريب الموارد.

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

من الإطلاق إلى المعالجة

في الصفحة السابقة تعلّمت كيف تُطلق (throw) استثناءً عندما يحدث خطأ ما. الإطلاق ليس سوى نصف القصة؛ فالاستثناء الذي لا يُلتقط أبدًا يستدعي std::terminate ويُعطّل برنامجك. عبارة try/catch هي الطريقة التي تعالج بها ما أُطلق وتُبقي البرنامج يعمل.

الشكل بسيط: ضع الكود المحفوف بالمخاطر داخل كتلة try، وأتبِعها بكتلة catch واحدة أو أكثر تتفاعل مع أنواع أخطاء محددة. إذا عملت كتلة try بسلاسة، تُتجاوَز كل كتلة catch. وفي اللحظة التي يُطلَق فيها شيء، ينتقل التحكم مباشرة إلى أول catch مطابقة.

لاحظ أن "after" لا تُطبع أبدًا. فبمجرد انطلاق throw، تُهجَر بقية كتلة try ويُستأنف التنفيذ داخل كتلة catch المطابقة. وبعد انتهاء catch، يواصل البرنامج عمله بشكل طبيعي أسفلها.

الالتقاط عبر مرجع ثابت

أهم عادة على الإطلاق في معالجة الأخطاء بلغة C++: التقط الاستثناءات عبر مرجع const، لا بالقيمة.

الالتقاط بالقيمة ينسخ الاستثناء، والأسوأ أنه يقتطعه (slicing). تُشكّل الاستثناءات القياسية تسلسلًا هرميًا (runtime_error وlogic_error كلاهما مشتق من std::exception)، لذا فإن التقاط استثناء مشتق بصفته قيمة أساسية يقطع الجزء المشتق. أما الالتقاط عبر مرجع فيُبقي الكائن سليمًا ومتعدد الأشكال:

هنا نُطلق out_of_range لكننا نلتقطه بصفته const exception&. وبما أن out_of_range مشتق من exception، يطابق معالِج الصنف الأساسي، ويعني المرجع أن e.what() ما زال يُرجع الرسالة الحقيقية. ولو كتبت catch (exception e) (بالقيمة)، لاقتُطع الكائن إلى exception بسيط ولربما فقدت الرسالة المحددة.

كتل catch متعددة

يمكن أن يتبع كتلة try واحدة عدةُ كتل catch، كل واحدة لنوع استثناء مختلف. تُجربها C++ من الأعلى إلى الأسفل وتُنفّذ أول كتلة مطابقة، لذا رتّبها من الأكثر تحديدًا إلى الأكثر عمومية.

بما أن invalid_argument أكثر تحديدًا من exception، يجب أن يأتي أولًا. ولو عكست الترتيب ووضعت catch (const exception&) في الأعلى، لابتلع كل استثناء، ولأصبح معالِج invalid_argument الذي تحته كودًا ميتًا لا يمكن أن يُنفَّذ أبدًا. تُحذّر العديد من المُصرِّفات من هذا، لكن اللغة لن تمنعك.

‏catch (...) وإعادة الإطلاق

أحيانًا تريد شبكة أمان لأي شيء لم تتوقعه. المعالِج الشامل catch (...) يطابق كل نوع استثناء، بما في ذلك ما لا يُشتق من std::exception (إذ يستطيع أحدهم كتابة throw 42; أو throw "oops";).

المشكلة أنك لا تحصل على أي كائن؛ فلا يوجد e لتفحصه. لذا فإن catch (...) يُستخدم على أفضل وجه كملاذ أخير: سجّل أن شيئًا ما قد فشل، أو نظّف وأعِد الإطلاق.

لـإعادة إطلاق الاستثناء الحالي - أي تمريره إلى معالِج خارجي بعد إجراء بعض التنظيف أو التسجيل المحلي - استخدم throw; مجردة بلا مُعامِل. هذا يحافظ على الاستثناء الأصلي (نوعه الحقيقي ورسالته)، بخلاف throw e; التي تعيد إطلاق نسخة مقتطعة:

يُسجّل المعالِج الداخلي ويعيد الإطلاق؛ ثم يتعامل المعالِج الخارجي في main معه. استخدم throw; المجردة لهذا، ولا تستخدم أبدًا throw e;.

فكّ المكدس وRAII

عندما ينتشر استثناء خارج كتلة try، تُجري C++ فكّ المكدس (stack unwinding): يُستدعى المُدمِّر لكل كائن محلي بين throw وكتلة catch المطابقة، بترتيب عكسي لترتيب الإنشاء. هذا ما يجعل الاستثناءات آمنة؛ إذ تُحرَّر الموارد التي تحتفظ بها كائنات المكدس تلقائيًا.

ولهذا السبب بالضبط ينبغي أن تحتفظ بالموارد في أنواع RAII (مثل std::vector وstd::string والمؤشرات الذكية) بدلًا من new/delete اليدويين. انظر ماذا يحدث حين يعبر استثناءٌ تخصيصًا يدويًا للذاكرة:

void leaky() {
    int* buffer = new int[1000];
    mightThrow();        // إن أطلق هذا استثناءً، فلن يُنفَّذ السطر التالي أبدًا...
    delete[] buffer;     // ...ويتسرّب الـ buffer
}

لأن throw يقفز فوق delete[]، تُفقَد الذاكرة. يحل المؤشر الذكي ذلك مجانًا؛ إذ يعمل مُدمِّره أثناء فكّ المكدس:

void safe() {
    auto buffer = std::make_unique<int[]>(1000);
    mightThrow();   // إن أطلق هذا استثناءً، فمُدمِّر buffer يحرّر الذاكرة على أي حال
}                   // لا delete يدوي، ولا تسريب، حتى على مسار الاستثناء

الخلاصة: لا تحاول catch التقاط استثناء لمجرد عمل delete لشيء ما. دع المُدمِّرات تتولى التنظيف، واحتفظ بـ catch للقرارات حول كيفية التعافي.

أخطاء ومزالق شائعة

تتكرر حفنة من المزالق مرارًا وتكرارًا:

لا تستخدم الاستثناءات لتدفق التحكم الاعتيادي. فالإطلاق وفكّ المكدس أبطأ بكثير من if بسيطة. احتفظ بالاستثناءات لحالات الخطأ الاستثنائية فعلًا، لا لحالة مثل «أدخل المستخدم سلسلة فارغة».

كتلة catch فارغة تُخفي العلل. كتابة catch (...) {} لإسكات خطأٍ تعني اختفاء الإخفاقات دون أثر. على الأقل سجّل المشكلة؛ وعادةً ينبغي أن تعيد إطلاقها أو تعالجها كما يجب.

المُدمِّر الذي يُطلق استثناءً خطير. إذا أطلق مُدمِّرٌ استثناءً أثناء فكّ المكدس (بينما هناك استثناء آخر قيد التنفيذ بالفعل)، فإن البرنامج يستدعي std::terminate. والمُدمِّرات في C++ الحديثة هي noexcept ضمنيًا؛ فلا تدع أبدًا استثناءً يفلت من أحدها.

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

التالي: السلوك غير المحدَّد

الاستثناءات هي الطريقة المحدَّدة التي تُخبرك بها C++ بأن شيئًا ما قد ساء: تُطلق، وتلتقط، والسلوك متوقَّع. لكن لـ C++ أيضًا زاوية أكثر قتامة لا تقدّم فيها اللغة أي وعد على الإطلاق: إلغاء الإشارة إلى مؤشر معلّق، والقراءة بعد نهاية مصفوفة، وطفحان العدد الصحيح ذي الإشارة. تتناول الصفحة التالية السلوك غير المحدَّد: ما الذي يُطلقه، ولماذا قد يبدو وكأنه «يعمل» حتى اللحظة التي يفشل فيها فشلًا كارثيًا، وكيف تُبقيه بعيدًا عن كودك.

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

كيف يعمل try-catch في C++؟

تضع الكود الذي قد يُطلق استثناءً داخل كتلة try { }. إذا أُطلق استثناء، يتوقف البرنامج عن تنفيذ بقية كتلة try وينتقل إلى أول كتلة catch مطابقة، حيث تعالج الخطأ. وإن لم يُطلق أي شيء، تُتجاوَز كتل catch بالكامل.

لماذا ينبغي التقاط الاستثناءات عبر مرجع ثابت في C++؟

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

كيف تلتقط أي استثناء في C++؟

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

Coddy programming languages illustration

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

ابدأ الآن