عند تطوير برنامج لأجهزة التحكم الدقيق في 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 ;
أو هكذا:
ReturnCode result ; for(auto it: diagnostics) {
أو هكذا:
void CpuDiagnostic::SomeFunction(ReturnCode errocode) { Cpu_Error status = errorcode ; switch (status) { case CpuError::Alu:
كما ترون من التعليمات البرمجية ، يتم استخدام بعض فئة
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
، قادرًا على تحويل أي نوع من التعداد إلى عدد صحيح والعكس صحيح من عدد صحيح إلى نوع تعداد. بالإضافة إلى هذه الميزات ، من أجل أن تكون قادرة على إرجاع الفئة ، والقيمة الفعلية للرمز ، وكذلك تكون قادرة على التحقق:
الحل
يجب أن يخزن الفصل في حد ذاته رمز خطأ ورمز فئة ورمز يتوافق مع عدم وجود أخطاء والمشغل المصبوب ومشغل التعيين. الطبقة المقابلة هي كما يلي:

كود الصنف 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 {
من الضروري أن أشرح قليلاً ما يحدث هنا. لتبدأ منشئ القالب
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 {
يمكن أن يؤدي أيضًا إلى نوع تعداد ويسمح لنا بالقيام بما يلي:
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); }
سيسمح لنا ذلك بالتحقق مما إذا كان هناك أي خطأ في كود الإرجاع:
هذا هو كل شيء أساسا. يبقى السؤال في
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"
دعنا نحلل بمزيد من التفاصيل. أدنى وظيفة
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 . أي أنه في كل تكرار سيكون هناك شيء مشابه لهذا:
بمجرد انتهاء جميع الأنواع في القائمة ، لن تتمكن
std::enable_if_t
من استنتاج نوع القيمة
GetEnumPosition()
، وفي هذا التكرار سينتهي:
ماذا يحدث إذا كان النوع في القائمة. في هذه الحالة ، سيعمل فرع آخر ، الفرع 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
، فسيتم الحصول على التسلسل التالي:
في التكرار الثاني ، ينتهي استدعاء الدالة العودية ونحصل على 1 (من التكرار الأول) + 0 (من الثانية) =
1 في الإخراج - هذا فهرس للنوع Measure_Error في القائمة
EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>
نظرًا لأن هذه دالة
constexpr,
جميع العمليات الحسابية في مرحلة
constexpr,
ولا يتم إنشاء أي رمز فعليًا.
كل هذا لا يمكن كتابته ، يكون تحت تصرف C ++ 17. لسوء الحظ ، لا يدعم برنامج التحويل البرمجي IAR الخاص بي C ++ 17 بشكل كامل ، وبالتالي كان من الممكن استبدال مجموعة القدم بأكملها بالكود التالي:
يبقى الآن لجعل أساليب القالب
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
هناك شيء واحد فقط للقيام به:
تسجيل نوع تعدادك في القائمة.
ولا توجد حركات غير ضرورية ، لا يتحرك الكود الموجود ، وللإضافة تحتاج فقط إلى تسجيل النوع في القائمة. علاوة على ذلك ، سيتم كل ذلك في مرحلة الترجمة ، ولن يقوم المترجم بحساب جميع الفئات فحسب ، بل سيحذرك أيضًا إذا نسيت تسجيل النوع ، أو حاول تمرير نوع غير قابل للتعداد.
في الإنصاف ، تجدر الإشارة إلى أنه في تلك الـ 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 } ;
اطبع السطور التالية:
رمز الإرجاع: 3 فئة الإرجاع: 0
رمز الإرجاع: 3 فئة الإرجاع: 2
رمز الإرجاع: 2 فئة الإرجاع: 1
mError: 1