noexcept-ctcheck أو بعض وحدات الماكرو البسيطة لمساعدة المترجم على كتابة رمز noexcept

عند التطوير في C ++ ، يجب عليك كتابة التعليمات البرمجية من وقت لآخر حيث يجب ألا تحدث استثناءات. على سبيل المثال ، عندما نحتاج إلى كتابة مقايضة خالية من الاستثناءات للأنواع الأصلية أو تحديد عبارة نقل noexcept لفئتنا ، أو تنفيذ أداة تدمير غير يدوية يدويًا.


في الإصدار C ++ 11 ، تمت إضافة معدِّل noexcept إلى اللغة ، مما يتيح للمطور أن يفهم أنه لا يمكن استبعاد الاستثناءات من الوظيفة التي تحمل علامة noexcept. لذلك ، يمكن استخدام الوظائف التي تحمل هذه العلامة بأمان في السياقات التي لا ينبغي أن تنشأ فيها استثناءات.


على سبيل المثال ، إذا كان لدي هذه الأنواع والوظائف:


class first_resource {...}; class second_resource {...}; void release(first_resource & r) noexcept; void close(second_resource & r); 

وهناك فئة معينة من resources_owner التي تمتلك كائنات مثل first_resource و second_resource :


 class resources_owner { first_resource first_resource_; second_resource second_resource_; ... }; 

ثم يمكنني كتابة destructor resources_owner كالتالي:


 resources_owner::~resources_owner() noexcept { //  release()   ,    . release(first_resource_); //    close()   ,  //   try-catch. try{ close(second_resource_); } catch(...) {} } 

بطريقة ما ، جعل noexcept في C ++ 11 حياة مطور C ++ أسهل. لكن تطبيق noexcept الحالي في C ++ الحديثة له جانب واحد سيء ...


لا يساعد برنامج التحويل البرمجي على التحكم في محتويات وظائف noexcept وطرقه


لنفترض أنه في المثال أعلاه كنت مخطئًا: لسبب ما ، اعتبرت release() علامة على أنه noexcept ، ولكنه في الحقيقة ليس كذلك ويمكن أن يلقي استثناءات. هذا يعني أنه عندما أكتب destructor باستخدام مثل هذا release() رمي release() :


 resources_owner::~resources_owner() noexcept { release(first_resource_); //  try-catch   ... } 

ثم أتوسل إلى المتاعب. عاجلاً أم آجلاً ، سوف يلقي هذا release() استثناءًا وسوف يتعطل طلبي بالكامل بسبب std::terminate() يطلق عليه تلقائيًا. سيكون أسوأ من ذلك إن لم يكن تعطل تطبيقي ، ولكن شخصًا آخر ، استخدموا فيه مكتبتي مع مثل هذا التدمير المسبب للمشاكل لـ resources_owner .


أو اختلاف آخر من نفس المشكلة. لنفترض أنني لم أكن مخطئًا في أن release() وضع علامة على أنه غير معروف. كان كذلك.


تم وضع علامة عليه في الإصدار 1.0 من مكتبة تابعة لجهة خارجية أخذت منها first_resource والإصدار release() . وبعد ذلك ، بعد عدة سنوات ، قمت بالترقية إلى الإصدار 3.0 من هذه المكتبة ، ولكن في الإصدار 3.0 من release() لم يعد يحتوي على معدّل noexcept.


حسنا ماذا؟ النسخة الرئيسية الجديدة ، فإنها يمكن بسهولة كسر API.


الآن فقط ، على الأرجح ، سوف أنسى إصلاح تنفيذ برنامج إتلاف resources_owner . وإذا كان شخص آخر بدلاً مني يشارك في دعم resource_owner ، الذي لم يبحث أبدًا في هذا المدمر ، فمن المحتمل أن تمر التغييرات في توقيع release() دون أن يلاحظها أحد.


لذلك ، أنا شخصياً لا أحب حقيقة أن المحول البرمجي لا يحذر المبرمج بأي شكل من الأشكال من قيام المبرمج داخل طريقة / دالة noexcept باستدعاء طريقة / وظيفة لاستثناء الاستثناء.


سيكون من الأفضل أن يصدر المترجم مثل هذه التحذيرات.


انقاذ الغرق هو عمل الغرق انفسهم


حسنا ، المترجم لا يعطي أي تحذيرات. ولا يمكن فعل شيء حيال هذا المطور البسيط. لا تتعامل مع تعديلات برنامج التحويل البرمجي C ++ لاحتياجاتك الخاصة. خاصة إذا كان عليك استخدام برنامج التحويل البرمجي ليس واحدًا ، ولكن الإصدارات المختلفة من برامج التحويل البرمجي C ++ المختلفة.


هل من الممكن الحصول على مساعدة من المترجم دون الدخول في الجملونات؟ أي هل من الممكن عمل نوع من الأدوات للتحكم في محتويات طرق / وظائف noexcept ، حتى لو كانت طريقة dendro-fecal؟


يمكنك ذلك. قذرة ، ولكن ممكن.


من أين تنمو الساقين؟


تم اختبار الطريقة الموضحة في هذه المقالة في الممارسة العملية عند إعداد الإصدار التالي من خادم HTTP الصغير المدمج لدينا RESTinio .


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


لحسن الحظ ، في الممارسة العملية ، لم تظهر هذه المشاكل أبدًا ، ولكن الدين التقني قد تراكمت ويجب القيام بشيء حيال ذلك. وكان عليك أن تفعل شيئا مع الكود الذي تم كتابته بالفعل. أي يجب تحويل رمز nonexcept العامل إلى رمز noexcept العامل.


تم ذلك بمساعدة العديد من وحدات الماكرو ، والتي تم ترتيبها حسب الكود في الأماكن الصحيحة. على سبيل المثال ، حالة تافهة:


 template< typename Message_Builder > void trigger_error_and_close( Message_Builder msg_builder ) noexcept { // An exception from logger/msg_builder shouldn't prevent // a call to close(). restinio::utils::log_error_noexcept( m_logger, std::move(msg_builder) ); RESTINIO_ENSURE_NOEXCEPT_CALL( close() ); } 

وهنا جزء أقل تافهة:


 void reset() noexcept { RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.empty()); RESTINIO_STATIC_ASSERT_NOEXCEPT( m_context_table.pop_response_context_nonchecked()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front()); RESTINIO_STATIC_ASSERT_NOEXCEPT(m_context_table.front().dequeue_group()); RESTINIO_STATIC_ASSERT_NOEXCEPT(make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed)); for(; !m_context_table.empty(); m_context_table.pop_response_context_nonchecked() ) { const auto ec = make_asio_compaible_error( asio_convertible_error_t::write_was_not_executed ); auto & current_ctx = m_context_table.front(); while( !current_ctx.empty() ) { auto wg = current_ctx.dequeue_group(); restinio::utils::suppress_exceptions_quietly( [&] { wg.invoke_after_write_notificator_if_exists( ec ); } ); } } } 

لقد صافح استخدام وحدات الماكرو هذه عدة مرات ، مشيرًا إلى الأماكن التي كنت أتصورها عن غير قصد على أنها noexcept ، ولكنها لم تكن كذلك.


لذا فإن المنهج الموصوف أدناه ، بالطبع ، هو منضدة ذاتيا وذات عجلات مربعة ، لكنها تسير ... أعني أنها تعمل.


علاوة على ذلك ، سنناقش التطبيق الذي تم عزله عن كود RESTinio في مجموعة منفصلة من وحدات الماكرو.


جوهر النهج


جوهر النهج هو تمرير العبارة / المشغل (stmt) ، والتي تحتاج إلى التحقق من وجود noexcept ، في ماكرو معين. يستخدم هذا الماكرو static_assert(noexcept(stmt), msg) للتحقق من أن stmt هو noexcept ، ثم يستبدل stmt في التعليمة البرمجية.


في الأساس ، هذا هو:


 ENSURE_NOEXCEPT_STATEMENT(release(some_resource)); 

سيتم استبداله بشيء مثل:


 static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource); 

لماذا تم الاختيار لصالح وحدات الماكرو؟


من حيث المبدأ ، يمكن للمرء الاستغناء عن وحدات الماكرو ويمكن للمرء أن يكتب static_assert(noexcept(...)) مباشرة في الكود مباشرة قبل التحقق من الإجراءات. لكن لدى وحدات الماكرو على الأقل بعض الفضائل التي تقلب الميزان لصالح استخدام وحدات الماكرو على وجه التحديد.


أولاً ، تقليل وحدات الماكرو تكرار التعليمات البرمجية. هناك مقارنة:


 static_assert(noexcept(release(some_resource)), "release(some_resource) is expected to be noexcept"); release(some_resource); 

و


 ENSURE_NOEXCEPT_STATEMENT(release(some_resource)); 

من الواضح أنه مع وحدات الماكرو التعبير الرئيسي ، أي release(some_resource) مرة واحدة فقط. هذا يقلل من احتمال رمز "زحف" مع مرور الوقت ، مع مرافقته ، عندما تم إجراء تصحيح في مكان واحد ونسي في الثانية.


ثانياً ، يمكن بسهولة تعطيل وحدات الماكرو والشيكات المخفية خلفها بسهولة. قل ، إذا كانت وفرة static_assert-s تؤثر سلبًا على سرعة الترجمة (على الرغم من أنني لم ألاحظ مثل هذا التأثير). أو الأهم من ذلك ، عند تحديث بعض مكتبة الطرف الثالث ، يمكن أن ترش أخطاء التجميع من static_assert المخفية وراء وحدات الماكرو مباشرة مع النهر. يمكن أن يؤدي تعطيل وحدات الماكرو مؤقتًا إلى السماح بالتحديث السلس للرمز ، بما في ذلك وحدات التحقق من التحقق بالتتابع أولاً في ملف واحد ، ثم في الملف الثاني ، ثم في الملف الثالث ، إلخ.


حتى وحدات الماكرو ، على الرغم من أنها ميزة قديمة ومثيرة للجدل في C ++ ، في هذه الحالة بالذات ، يتم تبسيط حياة المطور.


الماكرو الرئيسي ENSURE_NOEXCEPT_STATEMENT


يتم تنفيذ الماكرو الرئيسي ENSURE_NOEXCEPT_STATEMENT بشكل تافه:


 #define ENSURE_NOEXCEPT_STATEMENT(stmt) \ do { \ static_assert(noexcept(stmt), "this statement is expected to be noexcept: " #stmt); \ stmt; \ } while(false) 

يتم استخدامه للتحقق من أن الأساليب / الدالات التي تم استدعاؤها هي في الواقع noexcept وأن مكالماتهم لا تحتاج إلى تأطير بواسطة كتل try-catch. على سبيل المثال:


 class some_complex_container { one_container first_data_part_; another_container second_data_part_; ... public: friend void swap(some_complex_container & a, some_complex_container & b) noexcept { using std::swap; //  swap  noexcept,    . ENSURE_NOEXCEPT_STATEMENT(swap(a.first_data_part_, b.first_data_part_)); ENSURE_NOEXCEPT_STATEMENT(swap(a.second_data_part_, b.second_data_part_)); ... } ... void clean() noexcept { //  clean()  noexcept,    . ENSURE_NOEXCEPT_STATEMENT(first_data_part_.clean()); ENSURE_NOEXCEPT_STATEMENT(second_data_part_.clean()); ... } ... }; 

بالإضافة إلى ذلك ، هناك أيضًا الماكرو ENSURE_NOT_NOEXCEPT_STATEMENT. يتم استخدامه للتأكد من أن كتلة try-catch إضافية مطلوبة حول المكالمة بحيث لا تنحرف الاستثناءات المحتملة:


 class some_resource_owner { some_resource resource_; ... public: ~some_resource_owner() noexcept { try { //  release   noexcept,  try-catch     //      . ENSURE_NOT_NOEXCEPT_STATEMENT(release(resource_)); } catch(...) {} ... } ... }; 

وحدات الماكرو المساعدة STATIC_ASSERT_NOEXCEPT و STATIC_ASSERT_NOT_NOEXCEPT


لسوء الحظ ، لا يمكن استخدام وحدات الماكرو ENSURE_NOEXCEPT_STATEMENT و ENSURE_NOT_NOEXCEPT_STATEMENT إلا للبيانات / العبارات ، ولكن ليس للتعبيرات التي تُرجع قيمة. أي لا يمكنك الكتابة باستخدام ENSURE_NOEXCEPT_STATEMENT مثل هذا:


 auto resource = ENSURE_NOEXCEPT_STATEMENT(acquire_resource(params)); 

لذلك ، لا يمكن استخدام ENSURE_NOEXCEPT_STATEMENT ، على سبيل المثال ، في الحلقات التي غالباً ما يتعين عليك فيها كتابة شيء مثل:


 for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...} 

وتحتاج إلى التأكد من أن المكالمات get_first() ، get_next() ، وكذلك تعيين قيم جديدة لأني لا ألقي استثناء.


لمكافحة مثل هذه الحالات ، تمت كتابة STATIC_ASSERT_NOEXCEPT و STATIC_ASSERT_NOT_NOEXCEPT من وحدات الماكرو ، والتي خلفها فقط static_assert s مخفية ولا شيء أكثر من ذلك. باستخدام وحدات الماكرو هذه ، يمكنني تحقيق النتيجة التي أحتاجها بطريقة ما (لم يتم التحقق من تجميع هذه القطعة المحددة):


 STATIC_ASSERT_NOEXCEPT(something.get_first()); STATIC_ASSERT_NOEXCEPT(something.get_first().get_next()); STATIC_ASSERT_NOEXCEPT(std::declval<decltype(something.get_first())>() = something.get_first().get_next()); for(auto i = something.get_first(); i != some_other_object; i = i.get_next()) {...} 

من الواضح أن هذا ليس هو الحل الأفضل أنه يؤدي إلى ازدواجية التعليمات البرمجية ويزيد من خطر "زحف" مع مزيد من الصيانة. ولكن كخطوة أولى ، تبين أن وحدات الماكرو البسيطة هذه مفيدة.


Noexcept - مكتبة ctcheck


عندما شاركت هذه التجربة على مدونتي وعلى Facebook ، تلقيت اقتراحًا لترتيب التطورات المذكورة أعلاه في مكتبة منفصلة. الذي تم القيام به: يحتوي github الآن على فحص صغير للوقت للمكتبة noexcept-compile-time (أو noexcept-ctcheck ، إذا قمت بحفظ الرسائل) . لذلك كل ما سبق يمكنك أن تأخذ وتجرب. صحيح أن أسماء وحدات الماكرو أطول قليلاً من تلك المستخدمة في المقالة. أي NOEXCEPT_CTCHECK_ENSURE_NOEXCEPT_STATEMENT بدلاً من ENSURE_NOEXCEPT_STATEMENT.


ما لم تحصل في noexcept-ctcheck (حتى الآن؟)


هناك رغبة في إنشاء الماكرو ENSURE_NOEXCEPT_EXPRESSION ، والذي يمكن استخدامه مثل هذا:


 auto resource = ENSURE_NOEXCEPT_EXPRESSION(acquire_resource(params)); 

في التقريب الأول ، قد يبدو مثل هذا:


 #define ENSURE_NOEXCEPT_EXPRESSION(expr) \ ([&]() noexcept -> decltype(auto) { \ static_assert(noexcept(expr), #expr " is expected to be noexcept"); \ return expr; \ }()) 

ولكن هناك شكوك غامضة بوجود بعض المآزق التي لم أفكر فيها. بشكل عام ، لم تصل اليدين بعد ENSURE_NOEXCEPT_EXPRESSION :(


وإذا كنت تحلم؟


حلمي القديم هو الحصول على كتلة noexcept في C ++ يتحقق فيها المترجم نفسه من إلقاء الاستثناءات وإصدار تحذيرات إذا كان من الممكن طرح استثناءات. يبدو لي أن هذا سيجعل من الأسهل لكتابة رمز آمن الاستثناء. وليس فقط في الحالات الواضحة المذكورة أعلاه (المبادلة ، مشغلي الحركة ، المدمرات). على سبيل المثال ، قد تساعد كتلة noexcept في هذه الحالة:


 void modify_some_complex_data() { //   . one_container_.modify(); // ,   . ,      . //         try. noexcept { current_age_.increment(); } //    ,      . try { another_container_.modify(); ... } catch(...) { noexcept { //  ,     . current_age_.decrement(); one_container_.rollback_modifications(); } throw; } } 

هنا ، للتأكد من صحة التعليمات البرمجية ، من المهم جدًا ألا تؤدي الإجراءات التي يتم تنفيذها داخل كتل noexcept إلى استثناءات. وإذا كان بإمكان المترجم تتبع ذلك ، فسيكون ذلك مساعدة جدية للمطور.


ولكن ربما تكون كتلة noexcept مجرد حالة خاصة لمشكلة أكثر عمومية. وهي: التحقق من توقعات المبرمج بأن بعض الكود من التعليمات البرمجية له خصائص معينة. سواء كان ذلك بسبب الاستثناءات ، أو عدم وجود آثار جانبية ، أو عدم التكرار ، أو سباقات البيانات ، إلخ.


تأملات حول هذا الموضوع قبل عامين أدت إلى فكرة ضمنية وتتوقع سمات . هذه الفكرة لم تذهب أبعد من نشر المدونة ، لأن بينما تكمن بعيدا عن اهتماماتي الحالية والفرص. ولكن فجأة سيكون الأمر ممتعًا لشخص ما وسيدفع شخص ما لإنشاء شيء أكثر قابلية للحياة.


استنتاج


في هذه المقالة ، حاولت أن أتحدث عن تجربتي في تبسيط كتابة التعليمات البرمجية آمنة الاستثناء. باستخدام وحدات الماكرو ، بطبيعة الحال ، لا يجعل رمز أكثر جمالا والمدمجة. لكنها لا تعمل. وحتى وحدات الماكرو البدائية هذه تزيد من معاملتي للنوم المريح بشكل كبير. لذا ، إذا لم يفكر شخص آخر في كيفية التحكم في محتويات طرق / وظائف noexcept الخاصة بهم ، فربما تلهمك هذه المقالة للتفكير في هذا الموضوع.


وإذا وجد شخص ما طريقة لتبسيط حياته عند كتابة رمز noexcept ، فسيكون من المثير للاهتمام معرفة ماهية هذه الطريقة ، ومدى مساعدتها وفي أيها لا. وما مدى رضاك ​​عن ما تستخدمه.

Source: https://habr.com/ru/post/ar466849/


All Articles