
قبل أسبوعين كان المؤتمر الرئيسي في عالم C ++ - CPPCON .
كانت هناك خمسة أيام متتالية من الساعة الثامنة صباحاً حتى العاشرة مساءً. ناقش المبرمجون من جميع الأديان مستقبل C ++ والدراجات المسمومة وفكروا في كيفية جعل C ++ أسهل.
والمثير للدهشة أنه تم تخصيص العديد من التقارير لمعالجة الأخطاء. لا تسمح لك المناهج الراسخة بتحقيق أقصى أداء أو يمكن أن تولد أوراق تعليمات برمجية.
ما هي الابتكارات التي تنتظرنا في C ++ 2a؟
جزء من النظرية
تقليديا ، يمكن تقسيم جميع المواقف الخاطئة في البرنامج إلى مجموعتين كبيرتين:
- أخطاء فادحة.
- ليست أخطاء فادحة أو متوقعة.
أخطاء فادحة
بعدهم ، لا معنى لمواصلة التنفيذ.
على سبيل المثال ، يؤدي هذا إلى إلغاء الإشارة إلى مؤشر فارغ أو المرور عبر الذاكرة أو القسمة على 0 أو انتهاك الثوابت الأخرى في التعليمات البرمجية. كل ما يجب القيام به عند حدوثها هو توفير أقصى قدر من المعلومات حول المشكلة وإكمال البرنامج.
في سي ++ كثيرا هناك بالفعل طرق كافية لإكمال البرنامج:
بدأت المكتبات في الظهور لجمع بيانات عن الأعطال ( 1 ، 2 ، 3 ).
أخطاء غير فادحة
هذه هي الأخطاء التي يقدمها منطق البرنامج. على سبيل المثال ، الأخطاء عند العمل مع الشبكة ، وتحويل سلسلة غير صالحة إلى رقم ، إلخ. ظهور مثل هذه الأخطاء في البرنامج في ترتيب الأشياء. من أجل معالجتها ، هناك العديد من التكتيكات المقبولة بشكل عام في C ++.
سنتحدث عنها بمزيد من التفصيل باستخدام مثال بسيط:
دعونا نحاول كتابة وظيفة void addTwo()
باستخدام طرق مختلفة لمعالجة الأخطاء.
يجب أن تقرأ الدالة سطرين ، وتحويلها إلى int
وطباعة المجموع. تحتاج إلى معالجة أخطاء الإدخال / الإخراج والتجاوز والتحويل إلى الرقم. سأحذف تفاصيل التنفيذ غير المثيرة للاهتمام. سننظر 3 طرق رئيسية.
1. استثناءات
// // IO std::runtime_error std::string readLine(); // int // std::invalid_argument int parseInt(const std::string& str); // a b // std::overflow_error int safeAdd(int a, int b); void addTwo() { try { std::string aStr = readLine(); std::string bStr = readLine(); int a = parseInt(aStr); int b = parseInt(bStr); std::cout << safeAdd(a, b) << std::endl; } catch(const std::exeption& e) { std::cout << e.what() << std::endl; } }
تسمح لك الاستثناءات في C ++ بالتعامل مع الأخطاء بشكل مركزي دون الحاجة إلى
غير الضرورية ،
ولكن عليك أن تدفع ثمن ذلك مع مجموعة كبيرة من المشاكل.
- النفقات العامة في معالجة الاستثناءات كبيرة جدًا ؛ لا يمكنك غالبًا طرح استثناءات.
- من الأفضل عدم إلقاء استثناءات من المنشئين / المدمرين ومراقبة RAII.
- من خلال التوقيع على الوظيفة ، من المستحيل فهم الاستثناء الذي يمكن أن يخرج من الوظيفة.
- يزداد حجم الملف الثنائي بسبب رمز دعم استثناء إضافي.
2. رموز الإرجاع
النهج الكلاسيكي الموروث من C.
bool readLine(std::string& str); bool parseInt(const std::string& str, int& result); bool safeAdd(int a, int b, int& result); void processError(); void addTwo() { std::string aStr; int ok = readLine(aStr); if (!ok) { processError(); return; } std::string bStr; ok = readLine(bStr); if (!ok) { processError(); return; } int a = 0; ok = parseInt(aStr, a); if (!ok) { processError(); return; } int b = 0; ok = parseInt(bStr, b); if (!ok) { processError(); return; } int result = 0; ok = safeAdd(a, b, result); if (!ok) { processError(); return; } std::cout << result << std::endl; }
لا تبدو جيدة جدا؟
- لا يمكنك إرجاع القيمة الفعلية للدالة.
- من السهل جدًا نسيان معالجة الخطأ (آخر مرة قمت فيها بالتحقق من رمز الإرجاع من printf؟).
- يجب عليك كتابة رمز معالجة الخطأ بجانب كل وظيفة. من الصعب قراءة هذا الرمز.
سيؤدي استخدام C ++ 17 و C ++ 2a إلى إصلاح جميع هذه المشاكل بالتسلسل.
3. C ++ 17 و nodiscard
nodiscard
في C ++ 17.
إذا حددتها قبل إعلان الوظيفة ، فإن عدم التحقق من قيمة الإرجاع سيؤدي إلى تحذير المترجم.
[[nodiscard]] bool doStuff(); doStuff();
يمكنك أيضًا تحديد nodiscard
لفئة أو بنية أو فئة تعداد.
في هذه الحالة ، يمتد إجراء السمة إلى جميع الوظائف التي تُرجع قيمًا من النوع المسمى nodiscard
.
enum class [[nodiscard]] ErrorCode { Exists, PermissionDenied }; ErrorCode createDir(); /* ... */ createDir();
لن أقدم كود مع nodiscard
.
C ++ 17 std :: اختياري
في C ++ 17 ، std::optional<T>
.
دعونا نرى كيف يبدو الرمز الآن.
std::optional<std::string> readLine(); std::optional<int> parseInt(const std::string& str); std::optional<int> safeAdd(int a, int b); void addTwo() { std::optional<std::string> aStr = readLine(); std::optional<std::string> bStr = readLine(); if (aStr == std::nullopt || bStr == std::nullopt){ std::cerr << "Some input error" << std::endl; return; } std::optional<int> a = parseInt(*aStr); std::optional<int> b = parseInt(*bStr); if (!a || !b) { std::cerr << "Some parse error" << std::endl; return; } std::optional<int> result = safeAdd(*a, *b); if (!result) { std::cerr << "Integer overflow" << std::endl; return; } std::cout << *result << std::endl; }
يمكنك إزالة الحجج الداخلية من الوظائف وستصبح الشفرة أنظف.
ومع ذلك ، نحن نفقد معلومات الخطأ. أصبح من غير الواضح متى وما الخطأ الذي حدث.
يمكنك استبدال std::optional
بـ std::variant<ResultType, ValueType>
.
معنى الرمز هو نفسه مع std::optional
، ولكنه أكثر تعقيدًا.
المتوقع C ++ 2a و std ::
std::expected<ResultType, ErrorType>
- نوع قالب خاص ، من المحتمل أن يقع في أقرب معيار غير مكتمل.
لديها معلمتان.
كيف يختلف هذا عن variant
المعتاد؟ ما الذي يجعلها مميزة؟
std::expected
سيكون monad .
يقترح دعم مجموعة من العمليات على std::expected
كما هو على catch_error
: map
، catch_error
، bind
، catch_error
، return
، then
.
باستخدام هذه الوظائف ، يمكنك ربط استدعاءات الدوال بسلسلة.
getInt().map([](int i)return i * 2;) .map(integer_divide_by_2) .catch_error([](auto e) return 0; );
افترض أن لدينا وظائف مع إرجاع std::expected
.
std::expected<std::string, std::runtime_error> readLine(); std::expected<int, std::runtime_error> parseInt(const std::string& str); std::expected<int, std::runtime_error> safeAdd(int a, int b);
يوجد أدناه رمز زائف فقط ؛ لا يمكن إجباره على العمل في أي مترجم حديث.
يمكنك محاولة الاقتراض من Haskell قواعد النحو لتسجيل العمليات على monads. لماذا لا تسمح لها بذلك:
std::expected<int, std::runtime_error> result = do { auto aStr <- readLine(); auto bStr <- readLine(); auto a <- parseInt(aStr); auto b <- parseInt(bStr); return safeAdd(a, b) }
يقترح بعض المؤلفين بناء الجملة التالي:
try { auto aStr = try readLine(); auto bStr = try readLine(); auto a = try parseInt(aStr); auto b = try parseInt(bStr); std::cout result << std::endl; return safeAdd(a, b) } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; return 0; }
يقوم المترجم تلقائيًا بتحويل كتلة التعليمات البرمجية هذه إلى سلسلة من استدعاءات الوظائف. إذا لم ترجع الدالة في مرحلة ما ما هو متوقع منها ، فسوف تنكسر سلسلة الحساب. نعم ، std::runtime_error
خطأ ، يمكنك استخدام أنواع الاستثناءات الموجودة بالفعل في المعيار: std::runtime_error
، std::out_of_range
، إلخ.
إذا كان بإمكانك تصميم بناء الجملة جيدًا ، فسيسمح لك std::expected
بكتابة كود بسيط وفعال.
الخلاصة
لا توجد طريقة مثالية للتعامل مع الأخطاء. حتى وقت قريب ، في C ++ كانت هناك تقريبًا جميع الطرق الممكنة لمعالجة الأخطاء باستثناء monads.
في C ++ 2a ، من المرجح أن تظهر جميع الطرق الممكنة.
ما تقرأه وترى في الموضوع
- الاقتراح الفعلي .
- الكلام عن std :: متوقع مع CPPCON .
- Andrei Alexandrescu حول std :: متوقع في C ++ روسيا .
- أكثر أو أقل مناقشة أخيرة للاقتراح على Reddit .