При разработке ПО для микроконтроллеров на С++ очень часто можно столкнуться с тем, что использование стандартной библиотеки может привести к нежелательным дополнительным расходам ресурсов, как ОЗУ, так и ПЗУ. Поэтому зачастую классы и методы из библиотеки std
не совсем подходят для реализации в микроконтроллере. Существуют также некоторые ограничения в использовании динамически выделяемой памяти, RTTI, исключений и так далее. В общем случае, чтобы писать компактный и быстрый код нельзя просто так взять библиотеку std
и начать пользоваться, скажем операторами типа typeid
, потому что необходима поддержка RTTI, а это уже накладные расходы, хоть и не очень большие.
Поэтому иногда приходится изобретать велосипеды, чтобы выполнить все эти условия. Таких задач немного, но они есть. В данном посте, хотелось бы рассказать про вроде бы как простую задачку — расширить коды возврата существующих подсистем в ПО для микроконтроллера.
enum class Cpu_Error
{
Ok,
Alu,
Rom,
Ram
} ;
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()) ;
}
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::erorr_code
, который собственно практически все это делает. Очень хорошо его назначение описано здесь: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 ;
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()
для каждого перечисления.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) ;
}
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 ;
}
EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>
constexpr,
то все вычисления делаются на этапе компиляции и собственно никакого кода не генериться.//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>;
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
К сожалению, не доступен сервер mySQL