统一的错误处理(微控制器的C ++选项)

在为使用C ++的微控制器开发软件时,经常可能会遇到这样的事实,即使用标准库会导致不希望的RAM和ROM资源额外成本。 因此, std库中的类和方法通常不太适合在微控制器中实现。 使用动态分配的内存,RTTI,异常等也有一些限制。 通常,为了编写紧凑而快速的代码,您不能只使用std库并开始使用typeid运算符,因为您需要RTTI支持,尽管这虽然不是很大,但却是一项开销。

因此,有时您必须重新发明轮子才能满足所有这些条件。 这样的任务很少,但是确实如此。 在本文中,我想谈谈一个看似简单的任务-扩展微控制器软件中现有子系统的返回码。

挑战赛


假设您有一个CPU诊断子系统,并且它具有可枚举的返回码,请说明以下内容:

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

如果CPU诊断子系统检测到其中一个CPU模块(例如ALU或RAM)发生故障,则它必须返回相应的代码。

对于另一个子系统,同样,将其作为测量诊断,检查测量值是否在范围内并且通常有效(不等于NAN或Infinity):

 enum class Measure_Error { OutOfLimits, Ok, BadCode } ; 

对于每个子系统,都有一个GetLastError()方法,该方法返回该子系统的枚举错误类型。 对于CpuDiagnostic将返回MeasureDiagnostic类型CpuDiagnostic代码,对于CpuDiagnostic将返回MeasureDiagnostic类型MeasureDiagnostic代码。

并且有一个特定的日志,当发生错误时,应该记录错误代码。
为了理解,我将以一种非常简化的形式编写该代码:

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

显然,将枚举类型转换为整数时,对于不同类型,我们可以获得相同的值。 如何区分第一个错误代码是Cpu诊断子系统和第二个测量子系统的错误代码?

寻找解决方案


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 ,实际上几乎完成了所有这一切。 这里很好地描述了它的目的:

你自己的std :: code_error
支持C ++中的系统错误
“未来的C ++”中的确定性异常和错误处理

主要的抱怨是要使用该类,我们需要继承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添加到其主体,该主体在编译时将使用std::is_enum检查传递给构造函数的类型,并使用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类,第二个用于枚举类型,表明它是一个很好的返回码,因为我们将其与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() ,并且开发人员不必为每个枚举提供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_Error0 ,对于Measure_Error1 ,对于My_Error2 。 仍然迫使编译器自动进行计算。 对于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 ..检查请求的类型是否与模板列表中的第一个类型匹配,如果不匹配,则GetEnumPosition函数的返回类型将为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-这是列表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") ; } 

它是一个模板,如果TMeasure_Error则意味着对于GetCategory(Measure_Error)类型,调用了GetCategory(Measure_Error)方法模板的实例化,该方法模板又调用了类型为Measure_ErrorGetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>)返回Measure_Error在列表中的位置。 位置是1 。 实际上,在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%的代码中,枚举具有不同的名称而不是Ok代码,您将必须对此类型进行自己的专门化。

 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/zh-CN441956/


All Articles