Ketika mengembangkan perangkat lunak untuk mikrokontroler di C ++, sangat sering terjadi fakta bahwa menggunakan pustaka standar dapat menyebabkan biaya sumber daya tambahan yang tidak diinginkan, baik RAM maupun ROM. Oleh karena itu, seringkali kelas dan metode dari perpustakaan
std
tidak cukup cocok untuk implementasi di mikrokontroler. Ada juga beberapa pembatasan penggunaan memori yang dialokasikan secara dinamis, RTTI, pengecualian, dan sebagainya. Secara umum, untuk menulis kode yang ringkas dan cepat, Anda tidak bisa hanya mengambil pustaka
std
dan mulai menggunakan, katakan operator
typeid
, karena Anda memerlukan dukungan RTTI, dan ini merupakan biaya overhead, walaupun tidak terlalu besar.
Karena itu, terkadang Anda harus menemukan kembali roda untuk memenuhi semua kondisi ini. Ada beberapa tugas seperti itu, tetapi ada. Dalam posting ini, saya ingin berbicara tentang tugas yang tampaknya sederhana - untuk memperluas kode pengembalian subsistem yang ada dalam perangkat lunak mikrokontroler.
Tantangan
Misalkan Anda memiliki subsistem diagnostik CPU dan memiliki kode pengembalian enumerable, ucapkan ini:
enum class Cpu_Error { Ok, Alu, Rom, Ram } ;
Jika subsistem diagnostik CPU mendeteksi kegagalan salah satu modul CPU, (misalnya, ALU atau RAM), ia harus mengembalikan kode yang sesuai.
Hal yang sama untuk subsistem lain, biarlah itu menjadi diagnosis pengukuran, memeriksa bahwa nilai yang diukur berada dalam kisaran dan secara umum valid (tidak sama dengan NAN atau Infinity):
enum class Measure_Error { OutOfLimits, Ok, BadCode } ;
Untuk setiap subsistem, biarkan ada metode
GetLastError()
yang mengembalikan tipe kesalahan yang disebutkan pada subsistem ini. Untuk
CpuDiagnostic
kode tipe
CpuDiagnostic
akan dikembalikan, untuk
MeasureDiagnostic
kode tipe
Measure_Error
.
Dan ada log tertentu yang, ketika terjadi kesalahan, harus mencatat kode kesalahan.
Untuk memahami, saya akan menulis ini dalam bentuk yang sangat sederhana:
void Logger::Update() { Log(static_cast<uint32_t>(cpuDiagnostic.GetLastError()) ; Log(static_cast<uint32_t>(measureDiagstic.GetLastError()) ; }
Jelas bahwa ketika mengkonversi tipe yang disebutkan ke integer, kita bisa mendapatkan nilai yang sama untuk tipe yang berbeda. Bagaimana membedakan bahwa kode kesalahan pertama adalah kode kesalahan dari subsistem diagnostik Cpu, dan subsistem pengukuran kedua?
Cari solusi
Akan logis untuk metode
GetLastError()
untuk mengembalikan kode yang berbeda untuk subsistem yang berbeda. Salah satu keputusan paling langsung di dahi adalah menggunakan rentang kode yang berbeda untuk setiap jenis yang disebutkan. Sesuatu seperti ini
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 } ; ...
Saya pikir kerugian dari pendekatan ini jelas. Pertama, banyak pekerjaan manual, Anda harus secara manual menentukan rentang dan mengembalikan kode, yang tentunya akan menyebabkan kesalahan manusia. Kedua, mungkin ada banyak subsistem, dan menambahkan enumerasi untuk setiap subsistem bukanlah pilihan sama sekali.
Sebenarnya, akan lebih bagus jika memungkinkan untuk tidak menyentuh transfer sama sekali, untuk memperluas kode mereka dengan cara yang sedikit berbeda, misalnya, untuk dapat melakukan ini:
ResultCode result = Cpu_Error::Ok ;
Atau lebih:
ReturnCode result ; for(auto it: diagnostics) {
Atau lebih:
void CpuDiagnostic::SomeFunction(ReturnCode errocode) { Cpu_Error status = errorcode ; switch (status) { case CpuError::Alu:
Seperti yang Anda lihat dari kode, beberapa kelas
ReturnCode
digunakan di sini, yang harus mengandung kode kesalahan dan kategorinya. Di perpustakaan standar ada kelas seperti
std::error_code
, yang sebenarnya melakukan hampir semua ini. Sangat baik tujuannya dijelaskan di sini:
Std :: code_error Anda sendiriDukungan untuk kesalahan sistem di C ++Pengecualian deterministik dan penanganan kesalahan dalam “C ++ of the future”Keluhan utama adalah bahwa untuk menggunakan kelas ini, kita perlu mewarisi
std::error_category
, yang jelas kelebihan beban untuk digunakan dalam firmware pada mikrokontroler kecil. Bahkan setidaknya menggunakan std :: string.
class CpuErrorCategory: public std::error_category { public: virtual const char * name() const; virtual std::string message(int ev) const; };
Selain itu, Anda juga harus mendeskripsikan kategori (nama dan pesan) untuk masing-masing jenis yang disebutkan secara manual. Dan juga kode yang menunjukkan tidak adanya kesalahan dalam
std::error_code
adalah 0. Dan ada beberapa kasus ketika untuk setiap jenis kode kesalahan akan berbeda.
Saya ingin tidak memiliki overhead kecuali untuk menambahkan nomor kategori.
Oleh karena itu, akan logis untuk "menciptakan" sesuatu yang akan memungkinkan pengembang untuk membuat gerakan minimum dalam hal menambahkan kategori untuk tipe yang disebutkan.
Pertama, Anda perlu membuat kelas yang mirip dengan
std::error_code
, dapat mengkonversi tipe enumerasi menjadi integer dan sebaliknya dari integer ke tipe enumerated. Plus untuk fitur-fitur ini, agar dapat mengembalikan kategori, nilai kode yang sebenarnya, serta dapat memeriksa:
Solusi
Kelas harus menyimpan sendiri kode kesalahan, kode kategori dan kode yang sesuai dengan tidak adanya kesalahan, operator transmisi, dan operator penugasan. Kelas yang sesuai adalah sebagai berikut:

Kode kelas 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 {
Perlu dijelaskan sedikit apa yang terjadi di sini. Untuk memulai dengan konstruktor template
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, " ") ; }
Ini memungkinkan Anda untuk membuat kelas objek dari jenis apa pun yang disebutkan:
ReturnCode result(Cpu_Error::Ok) ; ReturnCode result1(My_Error::Error1); ReturnCode result2(cpuDiagnostic.GetLatestError()) ;
Untuk memastikan bahwa konstruktor hanya dapat menerima tipe yang disebutkan,
static_assert
ditambahkan ke badannya, yang pada waktu kompilasi akan memeriksa tipe yang diteruskan ke konstruktor menggunakan
std::is_enum
dan
std::is_enum
kesalahan dengan teks yang jelas. Kode sebenarnya tidak dihasilkan di sini, ini semua untuk kompiler. Jadi sebenarnya ini adalah konstruktor kosong.
Konstruktor juga menginisialisasi atribut pribadi, saya akan kembali lagi nanti ...
Selanjutnya, operator pemeran:
template<class T> operator T() const {
Itu juga dapat mengarah hanya ke tipe yang disebutkan dan memungkinkan kita untuk melakukan hal berikut:
ReturnCode returnCode(Cpu_Error::Rom) ; Cpu_Error status = errorCode ; returnCode = My_Errror::Error2; My_Errror status1 = returnCode ; returnCode = myDiagnostic.GetLastError() ; MyDiagsonticError status2 = returnCode ;
Baik dan secara terpisah operator bool ():
operator bool() const { return (GetValue() != goodCode); }
Ini akan memungkinkan kami untuk secara langsung memeriksa apakah ada kesalahan dalam kode pengembalian:
Ini pada dasarnya semua. Pertanyaannya tetap pada fungsi
GetCategory()
dan
GetOkCode()
. Seperti yang Anda tebak, yang pertama ditujukan untuk tipe enumerasi untuk berkomunikasi entah bagaimana kategorinya ke kelas
ReturnCode
, dan yang kedua untuk tipe enumerasi untuk menunjukkan bahwa itu adalah kode pengembalian yang baik, karena kita akan membandingkannya dengan operator
bool()
.
Jelas bahwa fungsi-fungsi ini dapat disediakan oleh pengguna, dan kami dapat dengan jujur menyebutnya di konstruktor kami melalui mekanisme pencarian yang bergantung pada argumen.
Sebagai contoh:
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); }
Ini membutuhkan upaya tambahan dari pengembang. Kita perlu untuk setiap tipe yang disebutkan yang ingin kita kategorikan untuk menambahkan dua metode ini dan memperbarui enumerasi
CategoryError
.
Namun, keinginan kami adalah agar pengembang hampir tidak menambahkan apa pun pada kode dan tidak peduli tentang cara memperluas tipe yang disebutkan.
Apa yang bisa dilakukan.
- Pertama, itu bagus untuk kategori yang akan dihitung secara otomatis, dan pengembang tidak harus menyediakan implementasi metode
GetCategory()
untuk setiap enumerasi. - Kedua, dalam 90% kasus dalam kode kami, Ok digunakan untuk mengembalikan kode yang baik. Oleh karena itu, Anda dapat menulis implementasi umum untuk 90% ini, dan untuk 10% Anda harus melakukan spesialisasi.
Jadi, mari kita berkonsentrasi pada tugas pertama - perhitungan kategori otomatis. Gagasan yang disarankan oleh kolega saya adalah bahwa pengembang harus dapat mendaftarkan tipenya. Ini dapat dilakukan dengan menggunakan template dengan sejumlah variabel argumen. Nyatakan struktur seperti itu
template <typename... Types> struct EnumTypeRegister{};
Sekarang untuk mendaftarkan enumerasi baru, yang harus diperluas oleh suatu kategori, kita cukup mendefinisikan tipe baru
using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error>;
Jika tiba-tiba kita perlu menambahkan enumerasi lain, maka cukup tambahkan ke daftar parameter template:
using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>;
Jelas, kategori untuk cantuman kami mungkin merupakan posisi dalam daftar parameter templat, mis. untuk
Cpu_Error
adalah
0 , untuk
Measure_Error
adalah
1 , untuk
My_Error
adalah
2 . Tetap memaksa kompiler untuk menghitung ini secara otomatis. Untuk C ++ 14, kami melakukan ini:
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...>()) ; }
Apa yang sedang terjadi di sini. Singkatnya, fungsi
GetEnumPosition<T<>>
, dengan parameter input menjadi daftar tipe
EnumTypeRegister
enumerasi
EnumTypeRegister
, dalam kasus kami
EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>
, dan parameter template
T adalah tipe enumerasi yang indeksnya harus kita temukan dalam daftar ini, berjalan melalui daftar dan jika T cocok dengan salah satu jenis dalam daftar, mengembalikan indeksnya, jika tidak, pesan "Jenis tidak terdaftar dalam daftar EnumTypeRegister" ditampilkan
Mari kita analisa lebih detail. Fungsi terendah
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...>()) ; }
Di sini cabang
std::enable_if_t< !std::is_same ..
memeriksa apakah tipe yang diminta cocok dengan tipe pertama dalam daftar templat, jika tidak, maka jenis fungsi
GetEnumPosition
yang
GetEnumPosition
akan
tU32
dan kemudian badan fungsi dijalankan, yaitu, panggilan rekursif dari fungsi yang sama lagi. , sementara jumlah argumen templat menurun sebesar
1 , dan nilai kembali meningkat sebesar
1 . Artinya, pada setiap iterasi akan ada sesuatu yang mirip dengan ini:
Setelah semua tipe dalam daftar berakhir,
std::enable_if_t
tidak akan dapat menyimpulkan jenis nilai pengembalian fungsi
GetEnumPosition()
, dan pada iterasi ini akan berakhir:
Apa yang terjadi jika jenisnya ada di daftar. Dalam hal ini, cabang lain akan berfungsi, cabang 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 ; }
Di sini ada pemeriksaan untuk kebetulan jenis
std::enable_if_t< std::is_same ...
Dan jika, katakan pada input ada jenis
Measure_Error
, maka urutan berikut akan diperoleh:
Pada iterasi kedua, panggilan fungsi rekursif berakhir dan kami mendapatkan 1 (dari iterasi pertama) + 0 (dari yang kedua) =
1 pada output - ini adalah indeks tipe Measure_Error dalam daftar
EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>
Karena ini adalah fungsi
constexpr,
semua perhitungan dilakukan pada tahap kompilasi dan tidak ada kode yang benar-benar dihasilkan.
Semua ini tidak dapat ditulis, tersedia C ++ 17. Sayangnya, kompiler IAR saya tidak sepenuhnya mendukung C ++ 17, dan jadi mungkin untuk mengganti seluruh footcloth dengan kode berikut:
Tetap sekarang untuk membuat metode templat
GetCategory()
dan
GetOk()
, yang akan memanggil
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); }
Itu saja. Sekarang mari kita lihat apa yang terjadi dengan konstruksi objek ini:
ReturnCode result(Measure_Error::Ok) ;
Mari kita kembali ke konstruktor dari kelas
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") ; }
Ini adalah templat satu, dan jika
T
adalah
Measure_Error
yang berarti bahwa instantiasi dari
GetCategory(Measure_Error)
metode
GetCategory(Measure_Error)
dipanggil, untuk jenis
Measure_Error
, yang pada gilirannya memanggil
GetEnumPosition
dengan jenis
Measure_Error
,
GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>)
yang mengembalikan posisi
Measure_Error
dalam daftar. Posisi adalah
1 . Dan sebenarnya seluruh kode konstruktor pada instantiation tipe
Measure_Error
digantikan oleh kompiler dengan:
explicit ReturnCode(const Measure_Error initReturnCode): errorValue(1), errorCategory(1), goodCode(1) { }
Ringkasan
Untuk pengembang yang
ReturnCode
menggunakan
ReturnCode
hanya ada satu hal yang harus dilakukan:
Daftarkan jenis Anda yang disebutkan dalam daftar.
Dan tidak ada gerakan yang tidak perlu, kode yang ada tidak bergerak, dan untuk ekstensi Anda hanya perlu mendaftarkan jenis dalam daftar. Selain itu, semua ini akan dilakukan pada tahap kompilasi, dan kompiler tidak hanya akan menghitung semua kategori, tetapi juga memperingatkan Anda jika Anda lupa untuk mendaftarkan tipe, atau mencoba untuk melewati tipe yang tidak dapat dihitung.
Dalam keadilan, perlu dicatat bahwa dalam 10% dari kode di mana enumerasi memiliki nama yang berbeda dan bukan kode Ok, Anda harus membuat spesialisasi Anda sendiri untuk jenis ini.
template<> constexpr tU32 GetOk<MyError>(const MyError) { return static_cast<tU32>(MyError::Good) ; } ;
Saya memposting contoh kecil di sini:
contoh kodeSecara umum, ini adalah aplikasi:
enum class Cpu_Error { Ok, Alu, Rom, Ram } ; enum class Measure_Error { OutOfLimits, Ok, BadCode } ; enum class My_Error { Error1, Error2, Error3, Error4, Ok } ;
Cetak baris berikut:
Kode pengembalian: 3 Kategori pengembalian: 0
Kode pengembalian: 3 Kategori pengembalian: 2
Kode pengembalian: 2 Kategori pengembalian: 1
mError: 1