معالجة خطأ موحدة (خيار C ++ لأجهزة التحكم الدقيقة)

عند تطوير برنامج لأجهزة التحكم الدقيق في C ++ ، من الممكن في كثير من الأحيان مواجهة حقيقة أن استخدام المكتبة القياسية يمكن أن يؤدي إلى تكاليف إضافية غير مرغوب فيها للموارد ، سواء RAM و ROM. لذلك ، غالبًا ما تكون الفصول والأساليب من مكتبة std غير مناسبة تمامًا للتنفيذ في المتحكم الدقيق. هناك أيضًا بعض القيود على استخدام الذاكرة المخصصة ديناميكيًا ، RTTI ، والاستثناءات ، وما إلى ذلك. بشكل عام ، من أجل كتابة رمز مضغوط وسريع ، لا يمكنك فقط أخذ مكتبة std والبدء في استخدام ، قل typeid التشغيل typeid ، لأن دعم RTTI ضروري ، وهذا بالفعل حمل كبير ، وإن لم يكن كبيرًا جدًا.

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

التحدي


افترض أن لديك نظامًا فرعيًا لتشخيص وحدة المعالجة المركزية وله رموز إرجاع متعددة ، قل هذه:

 enum class Cpu_Error { Ok, Alu, Rom, Ram } ; 

إذا اكتشف النظام الفرعي لتشخيص وحدة المعالجة المركزية فشل إحدى وحدات CPU ، (على سبيل المثال ، ALU أو RAM) ، فسيتعين عليه إرجاع الرمز المقابل.

نفس الشيء بالنسبة لنظام فرعي آخر ، فليكن تشخيص القياس ، والتحقق من أن القيمة المقاسة في النطاق وأنها صالحة بشكل عام (لا تساوي NAN أو Infinity):

 enum class Measure_Error { OutOfLimits, Ok, BadCode } ; 

لكل نظام فرعي ، دع هناك طريقة GetLastError() تُرجع نوع الخطأ الذي تم تعداده لهذا النظام الفرعي. بالنسبة إلى CpuDiagnostic سيتم إرجاع رمز من النوع CpuDiagnostic ، أما بالنسبة إلى MeasureDiagnostic ، MeasureDiagnostic رمز من النوع Measure_Error .

وهناك سجل معين ، عند حدوث خطأ ، يجب تسجيل رمز الخطأ.
لفهم ، سأكتب هذا في شكل مبسط للغاية:

 void Logger::Update() { Log(static_cast<uint32_t>(cpuDiagnostic.GetLastError()) ; Log(static_cast<uint32_t>(measureDiagstic.GetLastError()) ; } 

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

البحث عن حلول


سيكون من المنطقي للأسلوب GetLastError() لإرجاع رمز مختلف لأنظمة فرعية مختلفة. أحد أكثر القرارات المباشرة في الجبهة هو استخدام نطاقات مختلفة من الرموز لكل نوع تم تعداده. شيء مثل هذا

 constexpr tU32 CPU_ERROR_ALU = 0x10000001 ; constexpr tU32 CPU_ERROR_ROM = 0x10000002 ; ... constexpr tU32 MEAS_ERROR_OUTOF = 0x01000001 ; constexpr tU32 MEAS_ERROR_BAD = 0x01000002 ; ... enum class Cpu_Error { Ok, Alu = CPU_ERROR_ALU, Rom = CPU_ERROR_ROM, Ram = CPU_ERROR_RAM } ; ... 

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

في الواقع ، سيكون من الرائع إذا كان من الممكن عدم لمس عمليات النقل على الإطلاق ، لتوسيع رموزها بطريقة مختلفة قليلاً ، على سبيل المثال ، لتكون قادرة على القيام بذلك:

 ResultCode result = Cpu_Error::Ok ; //GetLastError()   Cpu_Error result = cpuDiagnostic.GetLastError() ; if(result) //    { //       Logger::Log(result) ; } //GetLastError()   Measure_Error result = measureDiagnostic.GetLastError() ; if(result) //    { //       Logger::Log(result) ; } 

أو هكذا:

 ReturnCode result ; for(auto it: diagnostics) { //GetLastError()     result = it.GetLastError() ; if (result) //    { Logger::Log(result) ; //      } } 

أو هكذا:

 void CpuDiagnostic::SomeFunction(ReturnCode errocode) { Cpu_Error status = errorcode ; switch (status) { case CpuError::Alu: // do something ; break; .... } } 

كما ترون من التعليمات البرمجية ، يتم استخدام بعض فئة ReturnCode هنا ، والتي ينبغي أن تحتوي على كل من رمز الخطأ وفئتها. في المكتبة القياسية ، يوجد مثل هذا النوع من std::error_code ، والذي يقوم فعلياً بكل هذا تقريباً. جيد جدا ويرد وصف الغرض منه هنا:

الأمراض المنقولة جنسيا الخاصة بك :: code_error
دعم أخطاء النظام في C ++
استثناءات حتمية ومعالجة الأخطاء في "C ++ للمستقبل"

الشكوى الرئيسية هي أنه لاستخدام هذه الفئة ، نحتاج إلى وراثة std::error_category ، والتي من الواضح أنها محملة std::error_category طاقتها لاستخدامها في البرامج الثابتة على ميكروكنترولر صغيرة. حتى على الأقل باستخدام std :: string.

 class CpuErrorCategory: public std::error_category { public: virtual const char * name() const; virtual std::string message(int ev) const; }; 

بالإضافة إلى ذلك ، سيكون عليك أيضًا وصف الفئة (الاسم والرسالة) لكل نوع من أنواعها المذكورة يدويًا. وكذلك رمز يشير إلى عدم وجود خطأ في std::error_code هو 0. وهناك حالات محتملة عندما يكون لكل خطأ رمز الخطأ.
أود أن لا أحمل أي إضافة رقم فئة.

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

تحتاج أولاً إلى جعل فصل std::error_code ، قادرًا على تحويل أي نوع من التعداد إلى عدد صحيح والعكس صحيح من عدد صحيح إلى نوع تعداد. بالإضافة إلى هذه الميزات ، من أجل أن تكون قادرة على إرجاع الفئة ، والقيمة الفعلية للرمز ، وكذلك تكون قادرة على التحقق:

 //GetLastError()   CpuError ReturnCode result(cpuDiagnostic.GetLastError()) ; if(result) //    { ... } 

الحل


يجب أن يخزن الفصل في حد ذاته رمز خطأ ورمز فئة ورمز يتوافق مع عدم وجود أخطاء والمشغل المصبوب ومشغل التعيين. الطبقة المقابلة هي كما يلي:



كود الصنف
 class ReturnCode { public: ReturnCode() { } template<class T> explicit ReturnCode(const T initReturnCode): errorValue(static_cast<tU32>(initReturnCode)), errorCategory(GetCategory(initReturnCode)), goodCode(GetOk(initReturnCode)) { static_assert(std::is_enum<T>::value, "   ") ; } template<class T> operator T() const { //Cast to only enum types static_assert(std::is_enum<T>::value, "   ") ; return static_cast<T>(errorValue) ; } tU32 GetValue() const { return errorValue; } tU32 GetCategoryValue() const { return errorCategory; } operator bool() const { return (GetValue() != goodCode); } template<class T> ReturnCode& operator=(const T returnCode) { errorValue = static_cast<tU32>(returnCode) ; errorCategory = GetCategory(returnCode) ; goodCode = GetOk(returnCode) ; return *this ; } private: tU32 errorValue = 0U ; tU32 errorCategory = 0U ; tU32 goodCode = 0U ; } ; 


من الضروري أن أشرح قليلاً ما يحدث هنا. لتبدأ منشئ القالب

  template<class T> explicit ReturnCode(const T initReturnCode): errorValue(static_cast<tU32>(initReturnCode)), errorCategory(GetCategory(initReturnCode)), goodCode(GetOk(initReturnCode)) { static_assert(std::is_enum<T>::value, "   ") ; } 


يسمح لك بإنشاء فئة كائن من أي نوع تم تعداده:

 ReturnCode result(Cpu_Error::Ok) ; ReturnCode result1(My_Error::Error1); ReturnCode result2(cpuDiagnostic.GetLatestError()) ; 

للتأكد من أن المُنشئ يمكنه قبول نوع static_assert ، static_assert إضافة static_assert إلى static_assert ، والذي في وقت الترجمة سوف يتحقق من النوع الذي تم تمريره إلى المُنشئ باستخدام std::is_enum خطأ بنص واضح. لم يتم إنشاء رمز حقيقي هنا ، وهذا كل شيء للمترجم. في الواقع هذا منشئ فارغ.

يقوم المنشئ أيضًا بتهيئة السمات الخاصة ، وسأعود إلى هذا لاحقًا ...
علاوة على ذلك ، المشغل المدلى بها:

 template<class T> operator T() const { //Cast to only enum types static_assert(std::is_enum<T>::value, "   ") ; return static_cast<T>(errorValue) ; } 

يمكن أن يؤدي أيضًا إلى نوع تعداد ويسمح لنا بالقيام بما يلي:

  ReturnCode returnCode(Cpu_Error::Rom) ; Cpu_Error status = errorCode ; returnCode = My_Errror::Error2; My_Errror status1 = returnCode ; returnCode = myDiagnostic.GetLastError() ; MyDiagsonticError status2 = returnCode ; 

حسنا وبشكل منفصل المشغل bool ():

  operator bool() const { return (GetValue() != goodCode); } 

سيسمح لنا ذلك بالتحقق مما إذا كان هناك أي خطأ في كود الإرجاع:

 //GetLastError()   Cpu_Error ReturnCode result(cpuDiagnostic.GetLastError()) ; if(result) //    { ... } 

هذا هو كل شيء أساسا. يبقى السؤال في GetCategory() و GetOkCode() . كما قد ReturnCode ، فإن الفئة الأولى ReturnCode فئته بطريقة أو بأخرى بفئة ReturnCode ، والثاني للنوع التعداد للإشارة إلى أنه رمز إرجاع جيد ، لأننا سنقوم بمقارنته مع عامل التشغيل bool() .

من الواضح أنه يمكن للمستخدم توفير هذه الوظائف ، ويمكننا أن ندعوها بأمانة في مُنشئنا من خلال آلية البحث المعتمدة على الوسيطة.
على سبيل المثال:

 enum class CategoryError { Nv = 100, Cpu = 200 }; enum class Cpu_Error { Ok, Alu, Rom } ; inline tU32 GetCategory(Cpu_Error errorNum) { return static_cast<tU32>(CategoryError::Cpu); } inline tU32 GetOkCode(Cpu_Error) { return static_cast<tU32>(Cpu_Error::Ok); } 

هذا يتطلب جهدا إضافيا من المطور. نحتاج إلى كل نوع من الفئات التي نريد تصنيفها لإضافة هاتين الطريقتين وتحديث تعداد CategoryError .

ومع ذلك ، فإن رغبتنا هي أن يضيف المطور أي شيء تقريبًا إلى الكود وأن لا يكلف نفسه عناء كيفية توسيع نوعه المعدّد.
ما يمكن القيام به.

  • أولاً ، كان من الرائع حساب الفئة تلقائيًا ، ولن يتعين على المطور تقديم تطبيق GetCategory() لكل تعداد.
  • ثانياً ، في 90٪ من الحالات في الكود ، يتم استخدام Ok لإرجاع الكود الجيد. لذلك ، يمكنك كتابة تطبيق عام لـ 90٪ ، و 10٪ عليك القيام بالتخصص.

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

 template <typename... Types> struct EnumTypeRegister{}; //     

الآن لتسجيل رقم جديد ، والذي يجب توسيعه حسب الفئة ، فإننا ببساطة نحدد نوعًا جديدًا

 using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error>; 

إذا كنا نحتاج فجأة إلى إضافة تعداد آخر ، فقم ببساطة بإضافته إلى قائمة معلمات القالب:

 using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>; 

من الواضح أن الفئة الخاصة بقوائمنا قد تكون موضعًا في قائمة معلمات النماذج ، أي بالنسبة إلى Cpu_Error ، يساوي 0 ، أما بالنسبة لـ Measure_Error فهو 1 ، أما My_Error فهو 2 . يبقى لفرض المترجم لحساب هذا تلقائيا. بالنسبة إلى الإصدار C ++ 14 ، نقوم بهذا:

 template <typename QueriedType, typename Type> constexpr tU32 GetEnumPosition(EnumTypeRegister<Type>) { static_assert(std::is_same<Type, QueriedType>::value, "     EnumTypeRegister"); return tU32(0U) ; } template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(EnumTypeRegister<Type, Types...>) { return 0U ; } template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<!std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(EnumTypeRegister<Type, Types...>) { return 1U + GetEnumPosition<QueriedType>(EnumTypeRegister<Types...>()) ; } 

ما يجري هنا. باختصار ، الدالة GetEnumPosition<T<>> ، مع وجود معلمة الإدخال قائمة EnumTypeRegister تعدادها ، في حالتنا EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> ، ومعلمة القالب T هي نوع من التعداد الذي يجب أن نعثر على فهرسه في هذه القائمة ، يعمل من خلال القائمة وإذا كان T يطابق أحد الأنواع في القائمة ، يُرجع الفهرس الخاص به ، وإلا يتم عرض الرسالة "النوع غير مسجل في قائمة EnumTypeRegister"

 //..    constexpr EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> list //  GetEnumPosition<Measure_Error>(list) //   1 -    Measure_Error   . 

دعنا نحلل بمزيد من التفاصيل. أدنى وظيفة

 template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<!std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(TypeRegister<Type, Types...>) { return 1U + GetEnumPosition<QueriedType>(TypeRegister<Types...>()) ; } 

هنا الفرع std::enable_if_t< !std::is_same .. يتحقق مما إذا كان النوع المطلوب يتطابق مع النوع الأول في قائمة القوالب ، وإذا لم يكن كذلك ، فسيكون النوع الذي تم إرجاعه من دالة tU32 هو tU32 ثم يتم تنفيذ نص الدالة ، وهي استدعاء دالة من نفس الوظيفة مرة أخرى ، بينما يقل عدد وسيطات القالب بمقدار 1 ، بينما تزيد قيمة الإرجاع بمقدار 1 . أي أنه في كل تكرار سيكون هناك شيء مشابه لهذا:

 //Iteration 1, 1+: tU32 GetEnumPosition<T>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) //Iteration 2, 1+1+: tU32 GetEnumPosition<T>(EnumTypeRegister<Measure_Error, My_Error>) //Iteration 3, 1+1+1: tU32 GetEnumPosition<T>(EnumTypeRegister<My_Error>) 

بمجرد انتهاء جميع الأنواع في القائمة ، لن تتمكن std::enable_if_t من استنتاج نوع القيمة GetEnumPosition() ، وفي هذا التكرار سينتهي:

 //         GetEnumPosition<T>(TypeRegister<>) template <typename QueriedType, typename Type> constexpr tU32 GetEnumPosition(EnumTypeRegister<Type>) { static_assert(std::is_same<Type, QueriedType>::value, "     EnumTypeRegister"); return tU32(0U) ; } 

ماذا يحدث إذا كان النوع في القائمة. في هذه الحالة ، سيعمل فرع آخر ، الفرع c std::enable_if_t< std::is_same .. :

 template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(TypeRegister<Type, Types...>) { return 0U ; } 

يوجد هنا التحقق من مصادفة الأنواع std::enable_if_t< std::is_same ... وإذا حدث ذلك ، عند الإدخال ، هناك نوع Measure_Error ، فسيتم الحصول على التسلسل التالي:

 //Iteration 1, tU32 GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) { return 1U + GetEnumPosition<Measure_Error>(EnumTypeRegister<Measure_Error, My_Error>) } //Iteration 2: tU32 GetEnumPosition<Measure_Error>(EnumTypeRegister<Measure_Error, My_Error>) { return 0 ; } 

في التكرار الثاني ، ينتهي استدعاء الدالة العودية ونحصل على 1 (من التكرار الأول) + 0 (من الثانية) = 1 في الإخراج - هذا فهرس للنوع Measure_Error في القائمة EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>

نظرًا لأن هذه دالة constexpr, جميع العمليات الحسابية في مرحلة constexpr, ولا يتم إنشاء أي رمز فعليًا.

كل هذا لا يمكن كتابته ، يكون تحت تصرف C ++ 17. لسوء الحظ ، لا يدعم برنامج التحويل البرمجي IAR الخاص بي C ++ 17 بشكل كامل ، وبالتالي كان من الممكن استبدال مجموعة القدم بأكملها بالكود التالي:

 //for C++17 template <typename QueriedType, typename Type, typename... Types> constexpr tU32 GetEnumPosition(EnumTypeRegister<Type, Types...>) { //        if constexpr (std::is_same<Type, QueriedType>::value) { return 0U ; } else { return 1U + GetEnumPosition<QueriedType>(EnumTypeRegister<Types...>()) ; } } 

يبقى الآن لجعل أساليب القالب GetCategory() و GetOk() ، والتي سوف تستدعي GetEnumPosition .

 template<typename T> constexpr tU32 GetCategory(const T) { return static_cast<tU32>(GetEnumPosition<T>(categoryDictionary)); } template<typename T> constexpr tU32 GetOk(const T) { return static_cast<tU32>(T::Ok); } 

هذا كل شيء. دعونا الآن نرى ما يحدث مع بناء الكائن هذا:

 ReturnCode result(Measure_Error::Ok) ; 

دعنا نعود إلى مُنشئ فئة ReturnCode

 template<class T> explicit ReturnCode(const T initReturnCode): errorValue(static_cast<tU32>(initReturnCode)), errorCategory(GetCategory(initReturnCode)), goodCode(GetOk(initReturnCode)) { static_assert(std::is_enum<T>::value, "The type have to be enum") ; } 

إنه قالب واحد ، وإذا كان T هو Measure_Error مما يعني أن GetCategory(Measure_Error) إنشاء مثيل لـ GetCategory(Measure_Error) قالب الأسلوب ، لنوع Measure_Error ، والذي بدوره يستدعي GetEnumPosition بنوع Measure_Error ، GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) الذي يعرض موضع Measure_Error في القائمة. الموقف هو 1 . وفي الواقع ، Measure_Error استبدال رمز المنشئ بالكامل عند إنشاء مثيل لـ Measure_Error بواسطة المحول البرمجي بـ:

 explicit ReturnCode(const Measure_Error initReturnCode): errorValue(1), errorCategory(1), goodCode(1) { } 

ملخص


للمطور الذي ReturnCode استخدام ReturnCode هناك شيء واحد فقط للقيام به:
تسجيل نوع تعدادك في القائمة.

 // Add enum in the category using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>; 

ولا توجد حركات غير ضرورية ، لا يتحرك الكود الموجود ، وللإضافة تحتاج فقط إلى تسجيل النوع في القائمة. علاوة على ذلك ، سيتم كل ذلك في مرحلة الترجمة ، ولن يقوم المترجم بحساب جميع الفئات فحسب ، بل سيحذرك أيضًا إذا نسيت تسجيل النوع ، أو حاول تمرير نوع غير قابل للتعداد.

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

 template<> constexpr tU32 GetOk<MyError>(const MyError) { return static_cast<tU32>(MyError::Good) ; } ; 

قمت بنشر مثال صغير هنا: مثال على الكود

بشكل عام ، إليك تطبيق:

 enum class Cpu_Error { Ok, Alu, Rom, Ram } ; enum class Measure_Error { OutOfLimits, Ok, BadCode } ; enum class My_Error { Error1, Error2, Error3, Error4, Ok } ; // Add enum in the category list using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>; Cpu_Error CpuCheck() { return Cpu_Error::Ram; } My_Error MyCheck() { return My_Error::Error4; } int main() { ReturnCode result(CpuCheck()); //cout << " Return code: "<< result.GetValue() // << " Return category: "<< result.GetCategoryValue() << endl; if (result) //if something wrong { result = MyCheck() ; // cout << " Return code: "<< result.GetValue() // << " Return category: "<< result.GetCategoryValue() << endl; } result = Measure_Error::BadCode ; //cout << " Return code: "<< result.GetValue() // << " Return category: "<< result.GetCategoryValue() << endl; result = Measure_Error::Ok ; if (!result) //if all is Ok { Measure_Error mError = result ; if (mError == Measure_Error::Ok) { // cout << "mError: "<< tU32(mError) << endl; } } return 0; } 

اطبع السطور التالية:
رمز الإرجاع: 3 فئة الإرجاع: 0
رمز الإرجاع: 3 فئة الإرجاع: 2
رمز الإرجاع: 2 فئة الإرجاع: 1
mError: 1

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


All Articles