Унифицированная обработка ошибок (C++ вариант для микроконтроллеров)

    При разработке ПО для микроконтроллеров на С++ очень часто можно столкнуться с тем, что использование стандартной библиотеки может привести к нежелательным дополнительным расходам ресурсов, как ОЗУ, так и ПЗУ. Поэтому зачастую классы и методы из библиотеки 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 будет возвращен код типа Cpu_Error, для MeasureDiagnostic код типа Measure_Error.

    И есть некий журнал, который при возникновении ошибки должен логировать код ошибки.
    Для понимания я напишу это в очень упрощенном виде:

    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 и выдаст ошибку с понятным текстом. Реального кода тут не генерится, это все для компилятора. Так что по факту это пустой конструктор.

    Также конструктор инициализирует приватные атрибуты, я вернусь к этому позже…
    Далее оператор приведения:

    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() для каждого перечисления.
    • Во-вторых, в 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. Осталось заставить компилятор вычислить это автоматически. Для С++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 — являющимся перечисляемым типом, индекс которого мы должны найти в этом списке, пробегается по списку и в случае, если Т совпадает с одним из типов в списке возвращает его индекс, в противном случае выдается сообщение "«Тип не зарегистрирован в списке 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 — это и есть индекс типа Measure_Error в списке EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>

    Поскольку это функция constexpr, то все вычисления делаются на этапе компиляции и собственно никакого кода не генериться.

    Все это можно было не писать, будь в распоряжении С++17. К сожалению мой IAR компилятор не поддерживает в полной мере С++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), для типа Measure_Error, который в свою очередь вызывает GetEnumPosition с типом Measure_Error, GetEnumPosition<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 нужно сделать только одну вещь:
    Зарегистрировать свой перечисляемый тип в списке.

    // 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;
    }

    Выведет следующие строки:
    Return code: 3 Return category: 0
    Return code: 3 Return category: 2
    Return code: 2 Return category: 1
    mError: 1
    Поддержать автора
    Поделиться публикацией

    Комментарии 21

      +1
      В свое время задумывался об этом, но к сожалению уровня знания не хватало даже для возникновения идей реализации. Спасибо за предложенное решение, на досуге попробую в деле.
        0

        Похоже на std::variant для enum.

          0
          Вроде как variant для избавления от union в основном был сделан, немного другая задача им решалась. Но честно, с ним не разбирался, он из С++17 идет, а мой компилятор его не поддерживает.
          +1
          Всё очень здорово. Но я вот только не очень понимаю чем это для микроконтроллеров сильно лучше обычного сишного кода:
          // categories.h
          enum { error_category_shift=16, error_code_mask=(1<<error_category_shift)-1  };
          
          enum { cpu_category, measure_category, my_category };
          
          // cpu.h #include "categories.h"
          enum {
          	cpu_ok=cpu_category << error_category_shift,
          	cpu_alu,
          	cpu_rom,
          	cpu_ram
          };
          
          // measure.h #include "categories.h"
          enum {
          	measure_ok=measure_category << error_category_shift,
          	measure_outoflimits,
          	measure_badcode
          };
          
          // my.h #include "categories.h"
          enum {
          	my_ok=my_category << error_category_shift,
          	my_error1,
          	my_error2,
          	my_error3
          };
          
          // errorcodes.h
          int any_error(int code);
          int get_error_category(int code);
          const char* get_category_name(int category);
          const char* get_error_text(int code);
          

          int error_code;
          ...
          error_code=some_function();
          if (any_error(error_code)) {
          	log_error(error_code);
          }
          ...
          

          impl
          // in cpu
          const char* get_error_text_cpu(int code) {
          	switch(code) {
          	case cpu_ok:  return "cpu_ok";
          	case cpu_alu: return "cpu_alu";
          	case cpu_rom: return "cpu_rom";
          	case cpu_ram: return "cpu_ram";
          	}
          	return 0;
          }
          // in measure
          const char* get_error_text_measure(int code) {
          	switch(code) {
          	case measure_ok:  return "measure_ok";
          	case measure_outoflimits: return "measure_outoflimits";
          	case measure_badcode: return "measure_badcode";
          	}
          	return 0;
          }
          // in my
          const char* get_error_text_my(int code) {
          	switch(code) {
          	case my_ok:  return "my_ok";
          	case my_error1: return "my_error1";
          	case my_error2: return "my_error2";
          	case my_error3: return "my_error3";
          	}
          	return 0;
          }
          // in errorcodes
          int get_error_category(int code) { return code >> error_category_shift; }
          int any_error(int code) { return code & error_code_mask; }
          const char* get_category_name(int category) {
          	switch(category) {
          		case cpu_category: return "cpu_category";	
          		case measure_category: return "measure_category";
          		case my_category: return "my_category";
          	};
          	return 0;
          }
          const char* get_error_text(int code) {
          	const char* res;
          	res=get_error_text_cpu(code); if (res) return res;
          	res=get_error_text_measure(code); if (res) return res;
          	res=get_error_text_my(code); if (res) return res;
          	return 0;
          }
          

          Подобный код элементарно генерировать скриптом что бы не писать руками.

            +1
            Тут много отличий.
            1. Надо писать скрипт
            2. Каждый такой метод get_error, даже если он будет возвращать просто номер придется юнит тестить. В вашем примере, вообще ад для юнит теста — каждую ветку switchа, хотя она и сгенерирована автоматически. Но покрытие юнит тестами должно 100% иначе не пройдет сертификацию на безопасность. В примере же на С++ там этого делать не надо, так как методов(GetEnumPosition() аналог) таких нет в коде, они все посчитают на этапе компиляции.
            3. Ну и минорное методы занимают место, так как существуют в рантайме. Хотя справедливости ради, надо сказать, что их место на С++ занимает конструктор, который типизирован, но там только инициализация идет, т.е. по сути кода нет. Т.е. я полагаю, что на Си кода получится больше. Надо проверить.
              +1
              Таки да есть еще отличия:
              4. Не требуется современный компилятор C++
              5. Функции с текстом можно разложить по отдельным объектникам и сложить в библиотеку, и если они не используются то и не прилинкуются.
              6. Скрипт это для ленивых («моется только тот кому лень чесаться»), остальные могут писать руками. (скриптом можно и тесты генерить, причем много и быстро)
              7. Проще, короче, нагляднее и собирается быстрее.

              «сертификация на безопасность» это обязательное требование для любого кода?
              ps: компилятор тоже сертифицированный?
                0
                4. Ну С++11 не такой уже и современный. Хотя конечно, согласен, есть еще куча компиляторов, которые его не поддерживают
                5. Это если енумы совпадают, а если в новом проекте надо расширить енум, или ввести новый. В одном проекте один объектник, в другом — другой.
                6. Согласен, либо скрипт, либо руками писать. Одна из задач звучала, как практически «ничего не делать».
                7. На Си проще — согласен, также как и быстрее собирается. На С++ код выглядит страшненько, для нормального программиста встроенного софта.
                Плюсы С++ все равно перевешивают :) Кода то меньше в итоге в ПЗУ.
                  0
                  Сертификация, конечно не обязательна для любого кода. Только для устройств используемых в системах на которые распространяется IEC 61508.
                  Компилятор, да сертифицированный. IAR Certified by TÜV SÜD
              +2

              Кстати на C++14 можно тоже без рекурсии через constexpr.


              template <typename... Ts> class TypeList {
              private:
                static constexpr auto no_index = std::numeric_limits<size_t>::max();
              
                template <typename T> static constexpr size_t getIndexImpl() {
                  auto seq = {same<T, Ts>()...};
                  size_t i = 0u;
                  for (auto s : seq) {
                    if (s) {
                      return i;
                    }
                    i++;
                  }
                  return no_index;
                }
                template <typename U, typename V> static constexpr bool same() {
                  return std::is_same<U, V>::value;
                }
              
              public:
                template <typename T> static constexpr size_t getIndex() {
                  constexpr bool found = getIndexImpl<T>() != no_index;
                  static_assert(found, "Type T not found in TypeList");
                  return getIndexImpl<T>();
                }
              };
              
              using ClassList = TypeList<C1, C2>;
              
              static_assert(ClassList::getIndex<C1>() == 0, "Test failed");
              static_assert(ClassList::getIndex<C2>() == 1, "Test failed");
              

              Код: https://godbolt.org/z/FyR-Lw

                0
                Согласен, без рекурсии понятнее. И вообще по идее ей нельзя в наших приложения использовать, в данном случае использовали, только потому, что она выполняется во время компиляции и в код не попадает.
                  0

                  Да, но я не до конца понял как работает constexpr. Вроде не обязательно всегда именно if constexpr писать, потому что это больше для замены enable_if и свинае ада. Но я проверил что действительно в моем примере вызовы заменяются на константу. Но в общем случае это уже на усмотрение компилятора, т.е. если я присваиваю constexpr value что то, то выражение справа вычислится во время компиляции. Но всегда ли constexpr функции во время компиляции считается?

                    0

                    Отвечу сам себе. Да constexpr функция не обязательно вызывается во время компиляции. Её можно вызывать и во время выполнения. В случае если аргументы известны только в ран тайм, это удобно чтобы не писать две версии одной и той же функции.

                      0
                      Нет, далеко не всегда. Чтобы быть уверенным, что твоя функция отработает в compile-time нужно присваивать результат либо constexpr-переменным, либо передавать его в параметры шаблона. Мало того, функции из stl, например std::char_traits<>::length, будут вычислятся в compile-time, а твоя, точно такая же функция — в runtime.
                        0

                        Да, тогда в моем коде лучше получать index, потом сверять его в static_assert с no_index а потом return index. Тогда можно быть уверенным что оно посчитает в compile time.

                          0
                          Еще, это можно проверить оператором noexcept. Если функция не сможет бросить исключение, то это costrexpr функция. Но для микроконтроллера это плохо, там обычно экспешены отключены, поэтому static_assert нормальное решение.

                  +1
                  «При разработке ПО для микроконтроллеров на С++ очень часто можно столкнуться с тем, что использование стандартной библиотеки может привести к нежелательным дополнительным расходам ресурсов, как ОЗУ, так и ПЗУ. Поэтому зачастую классы и методы из библиотеки std не совсем подходят для реализации в микроконтроллере.» — поэтому люди некоторое время назад написали библиотеку etl (embedded template library), предназначенную заместить std в embedded systems.
                    0
                    Да, я смотрел её, к сожалению она не подходит нам по причине отсутствия сертификата SIL 3 по, ГОСТ IEC 61508-3-2018. IAR компилятор вместе с поставляемой с ним библиотекой имеет такой сертификат.
                      0
                      Да, этого момента я не учёл. Тогда да, нужно изобретать некоторые собственные аналоги.
                        0
                        получается вы свой код верифицируете по этому стандарту?
                          0
                          Сертифицируем ПО. Там не только верификация, но нужно еще следовать и процессам, например, написано, что должно быть юнит тестирование, выходом является отчет о юнит тестировании с логами, или например, должен быть процесс ревью кода, выход логи ревью, должна быть проверка стическим анализатором кода, на выходе отчет анализатора и описание каждого предупреждения. Должна быть прослеживаемость между требования к безопасности и архитектуры, покажите как это у вас сделано.Кроме того проверяется, что например нет рекурсий, динамически выделяемой памяти, есть диагностики ПЗУ, ОЗУ, АЛУ и так далее. Эксперт это все проверяет, за каждый пункт бал дается, чем больше пунктов выполнено, тем больше балов. Есть обязательные пункты…
                          Есть вариант проще, называется проверено временем… Как например РТОС сертифицируется, основывается на расчете отказов у большого количества пользователей за определенное время, скажем 3-5 лет. Но тоже надо чтобы на предприятии была система отслеживания отказов.
                      +1
                      Хорошая идея. Вместо EnumTypeRegister можно использовать std::tuple, вместо GetEnumPosition что-то типа
                      template <typename Tuple, typename T, template <typename, typename> typename Pred = std::is_same, size_t I = 0>
                      constexpr size_t tuple_index() noexcept
                      {
                      	if constexpr(I >= std::tuple_size_v<Tuple>)
                      		return I;
                      	else if constexpr(Pred<T, std::tuple_element_t<I, Tuple>>::value)
                      		return I;
                      	else 
                      		return tuple_index<Tuple, T, Pred, I + 1>();
                      }
                      

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое