اذهب إلى المحتوى

الاستثناءات Exceptions في Cpp


محمد بغات

إمساك الاستثناءات 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


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...