سلسلة ++c للمحترفين الدرس 38: الاعتراضات 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





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن