إمساك الاستثناءات Catching exceptions
تُستخدَم الكتلة try/catch
لإمساك الاستثناءات إذ توضع في القسم try
الشيفراتُ التي يُشتبه في أنّها قد ترفع استثناءً، فيما تتكفّل الشيفرة الموضوعة في الكتلة catch
بمعالجة الاستثناء حال رفعه.
#include <iostream> #include <string> #include <stdexcept> int main() { std::string str("foo"); try { str.at(10); // std::out_of_range محاولة الدخول إلى العنصر قد تؤدي إلى رفع } catch (const std::out_of_range &e) { // وتحتوي رسالة توضيحية std::exception موروثة من what() std::cout << e.what(); } }
يمكن استخدام عدة كتل catch
للتعامل مع أكثر من نوع ٍمن الاستثناءات، وفي حال استخدام عدة عبارات catch
فإنّّ آلية معالجة الاستثناءات ستحاول مطابقتها بحسب ترتيب ظهورها في الشيفرة:
std::string str("foo"); try { str.reserve(2); // std::length_error محاولة تخصيص سعة زائدة قد تؤدي إلى رفع str.at(10); // std::out_of_range محاولة الدخول إلى هذا العنصر سترفع } catch (const std::length_error &e) { std::cout << e.what(); } catch (const std::out_of_range &e) { std::cout << e.what(); }
يمكن إمساك أصناف الاستثناءات المشتقّة من صنف أساسي باستخدام عبارة catch
واحدة مخصّصة للصنف الأساسي. ويمكن استبدال العبارتين catch
الخاصتين بالاستثنائَين std::length_error
و std::out_of_range
في المثال أعلاه بعبارة catch
واحدة موجّهة للاستثناء std:exception
:
std::string str("foo"); try { str.reserve(2); // std::length_error محاولة تخصيص سِعة زائدة قد تؤدي إلى رفع str.at(10); // std::out_of_range محاولة الدخول إلى هذا العنصر سترفع } catch (const std::exception &e) { std::cout << e.what(); }
ونظرًا لأنّ عبارات catch
تُختبَر بالترتيب، فتأكد من كتابة عبارات catch
الأكثر تخصيصًا أولاً، وإلا فقد لا تُستدعى شيفرة الاستثناء المخصوصة أبدًا:
try { /* شيفرة ترفع استثناء */ } catch (const std::exception &e) { /* std::exception معالجة كل الاستثناءات من النوع */ } catch (const std::runtime_error &e) { }
لن تُنفَّذ الكتلة الأخيرة في الشيفرة السابقة لأن std::runtime_error
ترث من std::exception
، وقد أُمسكت كل استثناءات std::exception
من قِبل تعليمة catch
التي سبقتها. هناك حلّ آخر، وهو استخدام عبارة catch
تعالج كل الاستثناءات، على النحو التالي:
try { throw 10; } catch (...) { std::cout << "caught an exception"; }
إعادة رفع استثناء
قد ترغب أحيانًا أن تفعل شيئًا بالاستثناء الذي أمسكته، مثل كتابته في سجل الأخطاء، أو طباعة تحذير، ثمّ إعادة رفعه إلى النطاق الأعلى لكي يُعالج هناك. وهذا ممكن، إذ يمكنك إعادة رفع أيّ استثناء تمسكه، انظر:
try { ... // شيفرة هنا } catch (const SomeException &e) { std::cout << "caught an exception"; throw; }
يؤدّي استخدام throw;
بدون وسائط إلى إعادة رفع الاستثناء الممسوك حاليًا.
الإصدار ≥ C++ 11
لإعادة رفع استثناء std::exception_ptr
سبقت إدارته، توفّر مكتبة C++ القياسية دالّة rethrow_exception
يمكن استخدامها عبر تضمين ترويسة
#include <iostream> #include <string> #include <exception> #include <stdexcept> void handle_eptr(std::exception_ptr eptr) // لا حرج في التمرير بالقيمة { try { if (eptr) { std::rethrow_exception(eptr); } } catch (const std::exception &e) { std::cout << "Caught exception \"" << e.what() << "\"\n"; } } int main() { std::exception_ptr eptr; try { std::string().at(1); // std::out_of_range ًسيولّد هذا استثناء } catch (...) { eptr = std::current_exception(); // إمساك } handle_eptr(eptr); } // eptr هنا عند تدمير std::out_of_range سيُستدعى مدمّر
أفضل تطبيق: رفع الاستثناءات بالقيمة وإمساكها بالمراجع الثابتة
بشكل عام، يُوصى برفع الاستثناء بالقيمة (بدلاً من رفعه بالمؤشّر)، ولكن يوصى بإمساكه بالمراجع الثابتة.
try { // throw new std::runtime_error("Error!"); // لا تفعل هذا // سينشئ هذا كائن استثناء في الكومة، وسيتطلّب منك أن تمسك المؤشر وتدير الذاكرة بنفسك // وهذا قد يُسبِّب تسرّب الذاكرة throw std::runtime_error("Error!"); } catch (const std::runtime_error &e) { std::cout << e.what() << std::endl; }
أحد الأسباب التي تجعل الإمساك بالمرجع أفضل، هو أنّه يلغي الحاجة إلى إعادة بناء الكائن عند نقله إلى كتلة catch
(أو عند نشره إلى كتل catch
الأخرى)، كما يتيح الإمساك بالمرجع معالجة الاستثناءات بأسلوب تعدد الأشكال، ويتجنّب تشريح slicing الكائنات، لكن إن كنت تريد إعادة رفع استثناء مثل throw e;
، انظر المثال أدناه، فلا يزال بإمكانك تشريح الكائن لأنّ تعليمة throw e;
تنشئ نسخة من الاستثناء من النّوع المُصرّح عنه:
#include <iostream> struct BaseException { virtual const char *what() const { return "BaseException"; } }; struct DerivedException: BaseException { // اختيارية هنا "virtual" الكلمة المفتاحية virtual const char *what() const { return "DerivedException"; } }; int main(int argc, char **argv) { try { try { throw DerivedException(); } catch (const BaseException &e) { std::cout << "First catch block: " << e.what() << std::endl; // ==> First catch block: DerivedException throw e;
سيغير ذلك الاستثناء من DerivedException
إلى BaseException
، تابع:
} } catch (const BaseException &e) { std::cout << "Second catch block: " << e.what() << std::endl; // ==> Second catch block: BaseException } return 0; }
إذا كنت متأكدًا من أنك لن تفعل أيّ شيء يؤدي إلى تغيير الاستثناء (مثل إضافة معلومات أو تعديل رسالة الخطأ المرفقة بالاستثناء)، فإنّّ إمساك الاستثناء بمرجع ثابت سيسمح للمٌصرّف بإجراء بعض التحسينات، ولكن هذا لن يلغي بالضرورة الحاجة إلى تشريح الكائن (كما هو مُوضّح في المثال أعلاه).
اقتباستحذير: احذر من رفع استثناءات غير مقصودة في كتلة
catch
، خاصّة تلك المتعلّقة منها بتخصيص ذاكرةٍ أو موارد إضافية، فمثلًا قد يؤدّي إنشاءlogic_error
أوruntime_error
أو الأصناف الفرعية المشتقّة منهما إلى رفعbad_alloc
نتيجة نفاد الذاكرة عند نسخ السلسلة النصّية الخاصة بالاستثناء، وقد ترفع مجاري الدخل/الخرج استثناءً أثناء تسجيل الأخطاء logging باستخدام أقنعة الاستثناءات exception masks المرتبطة بها.
الاستثناءات المخصصة
ينبغي ألّا ترفع قيمًا خامًا raw values مثل استثناءات، بل استخدم أحد أصناف الاستثناءات القياسية أو اصنع واحدًا خاصًّا بك، ويُعدّ إنشاء صنف استثناء موروث من std::exception
أسلوبًا مستحسنًا. في المثال التالي، انظر صنف استثناء مخصّص يرث مباشرةً من std::exception
:
#include <exception> class Except: virtual public std::exception { protected: int error_number; ///< Error number int error_offset; ///< Error offset std::string error_message; ///< Error message public: /**Constructor (C++ STL string, int, int). * @param msg The error message * @param err_num Error number * @param err_off Error offset */ explicit Except(const std::string &msg, int err_num, int err_off): error_number(err_num), error_offset(err_off), error_message(msg) {}
المدمر: يكون وهميًا Virtual من أجل السماح بالتصنيف الفرعي، تابع:
virtual~Except() throw () {}
نعيد مؤشرًا يشير إلى وصف الخطأ الثابت، ومؤشرًا يشير إلى *const char
، وتحتوي الذاكرة الأساسية على الكائن Except
، ويجب ألا يحاول المستدعون أن يحرروا الذاكرة. انظر:
virtual const char *what() const throw () { return error_message.c_str(); } /**Returns error number. * @return #error_number */ virtual int getErrorNumber() const throw () { return error_number; } /**Returns error offset. *@return #error_offset */ virtual int getErrorOffset() const throw () { return error_offset; } };
هذا مثال تطبيقي:
try { throw (Except("Couldn't do what you were expecting", -12, -34)); } catch (const Except &e) { std::cout << e.what() << "\nError number: " << e.getErrorNumber() << "\nError offset: " << e.getErrorOffset(); }
بهذه الطريقة فإنّك لا ترفع رسالة خطأ وحسب، ولكن ترفع أيضًا بعض القيم الأخرى التي توضّح طبيعة الخطأ بالضبط، وهكذا يصبح التعامل مع الأخطاء أسهل وأيسر. يوجد صنف استثناء يتيح لك معالجة رسائل الخطأ بسهولة، وهو std::runtime_error
، تستطيع الاشتقاق من هذا الصنف على النحو التالي:
#include <stdexcept> class Except: virtual public std::runtime_error { protected: int error_number; ///< Error number int error_offset; ///< Error offset public: /**Constructor (C++ STL string, int, int). * @param msg The error message * @param err_num Error number * @param err_off Error offset */ explicit Except(const std::string &msg, int err_num, int err_off): std::runtime_error(msg) { error_number = err_num; error_offset = err_off; } /** المدمر * للسماح بالاشتقاق Virtual الكلمة المفتاحية */ virtual~Except() throw () {} /**Returns error number. * @return #error_number */ virtual int getErrorNumber() const throw () { return error_number; } /**Returns error offset. *@return #error_offset */ virtual int getErrorOffset() const throw () { return error_offset; } };
لاحظ أنّك لم تُعِدْ تعريف الدالّة what()
من الصنف الأساسي (std::runtime_error
)، أي أنّنا سنستخدم إصدار الصنف الأساسي من what()
، لكن لا شيء يمنعك من إعادة تعريفها إن أردت.
std::uncaught_exceptions
الإصدار ≥ C++ 17
تقدّم C++ 17 النوع int std::uncaught_exceptions()
(ليحلّ محلّ النوع bool std::uncaught_exception()
المحدود) لمعرّفة عدد الاستثناءات غير الممسوكة حاليًا، يتيح هذا للأصناف معرفة ما إذا كان الاستثناء قد دُمِّر أثناء فكّ المكدّس stack unwinding أم لا.
#include <exception> #include <string> #include <iostream> // تطبيق التغيير عند التدمير: // في حال رفع استثناء: Rollback. // غير ذلك: Commit. class Transaction { public: Transaction(const std::string &s): message(s) {} Transaction(const Transaction &) = delete; Transaction &operator=(const Transaction &) = delete; void Commit() { std::cout << message << ": Commit\n"; } void RollBack() noexcept(true) { std::cout << message << ": Rollback\n"; } // ... ~Transaction() { if (uncaughtExceptionCount == std::uncaught_exceptions()) { Commit(); // قد يُطرَح استثناء } else { // فك المكدّس الحالي RollBack(); } } private: std::string message; int uncaughtExceptionCount = std::uncaught_exceptions(); }; class Foo { public: ~Foo() { try { Transaction transaction("In ~Foo"); // حتى لو كان هناك استثناء غير ممسوك commit الاعتماد //... } catch (const std::exception &e) { std::cerr << "exception/~Foo:" << e.what() << std::endl; } } }; int main() { try { Transaction transaction("In main"); // RollBack (تراجع) Foo foo; // يعتمد المعاملة ~Foo //... throw std::runtime_error("Error"); } catch (const std::exception &e) { std::cerr << "exception/main:" << e.what() << std::endl; } }
الناتج:
In~Foo: Commit In main: Rollback exception / main: Error
استخدام كتلة try
في الدوال العادية
إليك المثال التوضيحي التالي:
void function_with_try_block() try { // شيفرة هنا } catch (...) { // شيفرة هنا }
والذي يكافئ:
void function_with_try_block() { try { // شيفرة هنا } catch (...) { // شيفرة هنا } }
لاحظ أنّه بالنسبة للمنشئات والمدمّرات فإنّّ السلوك يكون مختلفًا لأنّ كتلة catch
تعيد رفع استثناء على أيّ حال (الاستثناء الممسوك في حال لم يكن هناك رفع الاستثناء آخر في جسم كتلة catch
).
ويُسمح للدّالّة main
أن تحتوي على كتلة try
مثل أيّ دالّة أخرى، بيْد أنّ كتلة try
في الدالة main
لن تمسك الاستثناءات التي تحدث أثناء إنشاء المتغيّرات الساكنة غير المحلية، أو عند تدمير أيّ متغيّر ساكن، لكن بدلاً من ذلك تُستدعى std::terminate
.
الاستثناء المتداخل
الإصدار ≥ C++ 11
أثناء معالجة الاستثناء، يمكن إمساك استثناء عام من دالّة ذات مستوى منخفض (مثل خطأ في نظام الملفّات أو خطأ في نقل البيانات) ورفع استثناء عالي المستوى أكثر تخصيصًا يشير إلى أنّه قد تعذّر تقديم بعض العمليات عالية المستوى (مثل عدم القدرة على نشر صورة على الويب).
يسمح ذلك بالاستجابة للمشاكل الخاصّة بالعمليات عالية المستوى، كما يسمح للمبرمج بإيجاد مكان وقوع الاستثناء في التطبيق، الجانب السلبي في هذا الحل هو أنّ مكدّس الاستدعاءات callstack الخاص بالاستثناء سيُقتطَع، وسيُفقد الاستثناء الأصلي، ويفرض هذا على المطوّرين تضمين نصّ الاستثناء الأصلي يدويًا في آخر استثناء مُنشأ.
تهدف الاستثناء المتداخلة std::nested_exception
إلى حل هذه المشكلة عن طريق إرفاق استثناء منخفض المستوى، والذي يصف سبب رفع الاستثناء، باستثناء عالي المستوى، والذي يصف ما يعنيه الاستثناء في هذه الحالة بالذات.
يسمح الاستثناء المتداخل بتداخل الاستثناءات بفضل std::throw_with_nested
:
#include <stdexcept> #include <exception> #include <string> #include <fstream> #include <iostream> struct MyException { MyException(const std::string &message): message(message) {} std::string message; }; void print_current_exception(int level) { try { throw; } catch (const std::exception &e) { std::cerr << std::string(level, ' ') << "exception: " << e.what() << '\n'; } catch (const MyException &e) { std::cerr << std::string(level, ' ') << "MyException: " << e.message << '\n'; } catch (...) { std::cerr << "Unkown exception\n"; } } void print_current_exception_with_nested(int level = 0) { try { throw; } catch (...) { print_current_exception(level); } try { throw; } catch (const std::nested_exception &nested) { try { nested.rethrow_nested(); } catch (...) { print_current_exception_with_nested(level + 1); // (Recursion) ذاتية } } catch (...) { //فارغ // (Recursion) إنهاء الذاتية } } // دالة بسيطة تمسك استثناء وتغلّفه في استثناء متداخل void open_file(const std::string &s) { try { std::ifstream file(s); file.exceptions(std::ios_base::failbit); } catch (...) { std::throw_with_nested(MyException { "Couldn't open " + s }); } } // دالة بسيطة تمسك استثناء وتغلّفه في اسستثناء متداخل void run() { try { open_file("nonexistent.file"); } catch (...) { std::throw_with_nested(std::runtime_error("run() failed")); } } // تشغيل الدالة أعلاه وطباعة الاستثناء الممسوك int main() { try { run(); } catch (...) { print_current_exception_with_nested(); } }
الخرج المحتمل:
exception: run() failed MyException: Couldn 't open nonexistent.file exception: basic_ios::clear
إذا كنت تعمل حصرًا مع الاستثناءات الموروثة من std::exception
، فهناك إمكانية لتبسيط الشيفرة أكثر.
استخدام كتلة Try في المنشئات
الطريقة التالية هي الوحيدة لإمساك استثناء في قائمة مهيئ:
struct A: public B { A() try: B(), foo(1), bar(2) { // جسم المنشئ } catch (...) {
تُمسَك الاستثناءات الناجمة عن قائمة المهيئ والمنشئ هنا، وسيعاد رفع الاستثناء الممسوك عند عدم رفع أي استثناء، نتابع المثال:
} private: Foo foo; Bar bar; };
استخدام كتلة Try في المدمرات
انظر المثال التوضيحي التالي:
struct A { ~A() noexcept(false) try { // جسم المدمّر } catch (...) { // الاستثناءات الناجمة عن جسم المدمّر تُمسَك هنا // في حال عدم رفع أيّ استثناء هنا، فسيُعاد رفع الاستثناء الممسوك } };
رغم أنّه يمكن رفع استثناء من المدمّر، إلّا أنّه ينبغي التزام الحذر، ذلك أنّه في حال رفع استثناء في مدمّر مُستدعًى أثناء فكّ المكدّس، فإنّّ ذلك سيتسبّب في استدعاء std::terminate
.
هذا الدرس جزء من سلسلة دروس عن C++.
ترجمة -بتصرّف- للفصل Chapter 72: Exceptions من كتاب C++ Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.