Jenis-aman bekerja dengan register tanpa overhead di C ++ 17: metaprogramming berbasis nilai

C ++, karena pengetikannya yang ketat, dapat membantu programmer pada tahap kompilasi. Sudah ada cukup banyak artikel di hub yang menggambarkan bagaimana, menggunakan tipe, untuk mencapai ini, dan itu bagus. Tetapi dalam semua yang saya baca, ada satu kekurangan. Bandingkan dengan pendekatan ++ dan pendekatan C menggunakan CMSIS, yang akrab di dunia pemrograman mikrokontroler:


some_stream.set (Direction::to_periph) SOME_STREAM->CR |= DMA_SxCR_DIR_0 .inc_memory() | DMA_SxCR_MINC_Msk .size_memory (DataSize::word16) | DMA_SxCR_MSIZE_0 .size_periph (DataSize::word16) | DMA_SxCR_PSIZE_0 .enable_transfer_complete_interrupt(); | DMA_SxCR_TCIE_Msk; 

Segera jelas bahwa pendekatan C ++ lebih mudah dibaca, dan karena masing-masing fungsi menggunakan tipe tertentu, tidak mungkin salah. Pendekatan C tidak memeriksa validitas data, itu ada pada programmer. Sebagai aturan, kesalahan hanya dikenali saat debugging. Tetapi pendekatan c ++ tidak gratis. Faktanya, setiap fungsi memiliki akses sendiri ke register, sementara di C mask pertama-tama dikumpulkan dari semua parameter pada tahap kompilasi, karena ini semua adalah konstanta, dan ditulis ke register sekaligus. Selanjutnya, saya akan menjelaskan bagaimana saya mencoba menggabungkan keamanan jenis dengan ++ dengan meminimalkan akses case. Anda akan melihat itu jauh lebih sederhana daripada kedengarannya.


Pertama saya akan memberikan contoh bagaimana saya ingin terlihat. Sangat diharapkan bahwa ini tidak berbeda jauh dari pendekatan C ++ yang sudah akrab.


 some_stream.set( dma_stream::direction::to_periph , dma_stream::inc_memory , dma_stream::memory_size::byte16 , dma_stream::periph_size::byte16 , dma_stream::transfer_complete_interrupt::enable ); 

Setiap parameter dalam metode yang ditetapkan adalah tipe terpisah yang dapat Anda pahami di dalam register mana Anda ingin menuliskan nilai, yang berarti bahwa selama kompilasi Anda dapat mengoptimalkan akses ke register. Metode ini variadik, sehingga bisa ada sejumlah argumen, tetapi harus ada pemeriksaan bahwa semua argumen milik pinggiran ini.


Sebelumnya tugas ini agak rumit bagi saya, sampai saya menemukan video ini tentang metaprogramming berbasis nilai . Pendekatan metaprogramming ini memungkinkan Anda untuk menulis algoritma umum seolah-olah itu adalah kode plus biasa. Pada artikel ini saya hanya akan memberikan yang paling penting dari video untuk menyelesaikan masalah, ada banyak algoritma yang lebih umum.


Saya akan memecahkan masalah secara abstrak, bukan untuk periferal tertentu. Jadi, ada beberapa bidang register, saya akan menuliskannya secara bersyarat sebagai enumerasi.


 enum struct Enum1 { _0, _1, _2, _3 }; enum struct Enum2 { _0, _1, _2, _3 }; enum struct Enum3 { _0, _1, _2, _3, _4 }; enum struct Enum4 { _0, _1, _2, _3 }; 

Tiga yang pertama akan berhubungan dengan satu pinggiran, yang keempat ke yang lain. Jadi, jika Anda memasukkan nilai enumerasi keempat ke dalam metode pinggiran pertama, harus ada kesalahan kompilasi, sebaiknya dimengerti. Juga, 2 daftar pertama akan berhubungan dengan satu register, yang ketiga ke yang lain.


Karena nilai-nilai enumerasi tidak menyimpan apa pun kecuali nilai-nilai aktual, jenis tambahan diperlukan yang akan menyimpan, misalnya, masker untuk menentukan di bagian mana dari register enumerasi ini akan ditulis.


 struct Enum1_traits { static constexpr std::size_t mask = 0b00111; }; struct Enum2_traits { static constexpr std::size_t mask = 0b11000; }; struct Enum3_traits { static constexpr std::size_t mask = 0b00111; }; struct Enum4_traits { static constexpr std::size_t mask = 0b00111; }; 

Masih menghubungkan 2 jenis ini. Di sini chip sudah berguna untuk 20 standar , tetapi cukup sepele dan Anda dapat menerapkannya sendiri.


 template <class T> struct type_identity { using type = T; }; //    constexpr auto some_type = type_identity<Some_type>{}; //      using some_type_t = typename decltype(some_type)::type; #define TYPE(type_identity) typename decltype(type_identity)::type 

Intinya adalah bahwa Anda dapat membuat nilai dari jenis apa pun dan meneruskannya ke fungsi sebagai argumen. Ini adalah batu bata utama dari pendekatan metaprogramming berbasis nilai, di mana Anda harus mencoba untuk meneruskan informasi jenis melalui nilai-nilai, dan bukan sebagai parameter templat. Di sini saya mendefinisikan makro, tetapi saya adalah musuh mereka di c ++. Tapi dia lebih sedikit menulis lebih lanjut. Selanjutnya, saya akan memberikan penghitungan penghubung dan propertinya ke fungsi dan makro lain yang memungkinkan mengurangi jumlah tempel salinan.


 constexpr auto traits(type_identity<Enum1>) { return type_identity<Enum1_traits>{}; } #define MAKE_TRAITS_WITH_MASK(enum, mask_) struct enum##_traits { \ static constexpr std::size_t mask = mask_; \ }; \ constexpr auto traits(type_identity<enum>) { \ return type_identity<enum##_traits>{}; \ } 

Penting untuk mengaitkan bidang dengan register yang sesuai. Saya memilih hubungan melalui pewarisan, karena standar sudah memiliki std::is_base_of , yang akan memungkinkan Anda untuk menentukan hubungan antara bidang dan register yang sudah dalam bentuk umum. Anda tidak dapat mewarisi dari enumerasi, jadi kami mewarisi dari properti mereka.


 struct Register1 : Enum1_traits, Enum2_traits { static constexpr std::size_t offset = 0x0; }; 

Alamat tempat register berada disimpan sebagai offset dari awal pinggiran.


Sebelum menjelaskan pinggiran, perlu untuk berbicara tentang daftar jenis dalam pemrograman berbasis nilai. Ini adalah struktur yang cukup sederhana yang memungkinkan Anda untuk menyimpan beberapa jenis dan memberikannya berdasarkan nilai. Sedikit seperti type_identity , tetapi untuk beberapa tipe.


 template <class...Ts> struct type_pack{}; using empty_pack = type_pack<>; 

Anda dapat mengimplementasikan banyak fungsi constexpr untuk daftar ini. Implementasinya jauh lebih mudah dipahami daripada daftar jenis Alexandrescu yang terkenal (perpustakaan Loki). Berikut ini adalah contohnya.


Properti penting kedua dari pinggiran harus kemampuan untuk menemukannya baik pada alamat tertentu (dalam mikrokontroler) dan meneruskan alamat secara dinamis untuk pengujian. Oleh karena itu, struktur periferal akan menjadi boilerplate, dan sebagai parameter mengambil tipe yang akan menyimpan alamat spesifik periferal di bidang nilai. Parameter templat akan ditentukan dari konstruktor. Nah, metode yang ditetapkan, yang disebutkan sebelumnya.


 template<class Address> struct Periph1 { Periph1(Address) {} static constexpr auto registers = type_pack<Register1, Register2>{}; template<class...Ts> static constexpr void set(Ts...args) { ::set(registers, Address::value, args...); } }; 

Semua yang metode set lakukan adalah memanggil fungsi bebas, menyampaikan semua informasi yang diperlukan untuk algoritma umum.


Saya akan memberikan contoh tipe yang memberikan alamat ke pinggiran.


 //    struct Address { static constexpr std::size_t value = SOME_PERIPH_BASE; }; //    ,    struct Address { static inline std::size_t value; template<class Pointer> Address(Pointer address) { value = reinterpret_cast<std::size_t>(address); } }; 

Semua informasi untuk algoritme umum dipersiapkan, tetap menerapkannya. Saya akan memberikan teks fungsi ini.


 template<class...Registers, class...Args> constexpr void set(type_pack<Registers...> registers, std::size_t address, Args...args) { //       ,  value based  constexpr auto args_traits = make_type_pack(traits(type_identity<Args>{})...); //              static_assert(all_of(args_traits, [](auto arg){ return (std::is_base_of_v<TYPE(arg), Registers> || ...); }), "one of arguments in set method don`t belong to periph type"); //   ,      constexpr auto registers_for_write = filter(registers, [](auto reg){ return any_of(args_traits, [](auto arg){ //       o  reg? return std::is_base_of_v<TYPE(arg), TYPE(reg)>; }); }); //           foreach(registers_for_write, [=](auto reg){ auto value = register_value(reg, args...); auto offset = decltype(reg)::type::offset; write(address + offset, value); }); }; 

Menerapkan fungsi yang mengubah argumen (bidang register spesifik) menjadi type_pack cukup sepele. Biarkan saya mengingatkan Anda bahwa elipsis dari daftar tipe templat mengungkapkan daftar jenis yang dipisahkan oleh koma.


 template <class...Ts> constexpr auto make_type_pack(type_identity<Ts>...) { return type_pack<Ts...>{}; } 

Untuk memverifikasi bahwa semua argumen berhubungan dengan register yang ditransfer, dan oleh karena itu untuk perangkat tertentu, perlu untuk menerapkan algoritma all_of. Dengan analogi dengan pustaka standar, algoritma menerima daftar jenis dan fungsi predikat sebagai input. Kami menggunakan lambda sebagai fungsi.


 template <class F, class...Ts> constexpr auto all_of(type_pack<Ts...>, F f) { return (f(type_identity<Ts>{}) and ...); } 

Di sini, untuk pertama kalinya, ekspresi pemindaian 17 standar diterapkan. Inovasi inilah yang sangat menyederhanakan kehidupan mereka yang gemar metaprogramming. Dalam contoh ini, fungsi f diterapkan ke masing-masing jenis dalam daftar Ts, mengubahnya menjadi type_identity , dan hasil dari setiap panggilan dikumpulkan oleh I.


Di dalam static_assert , algoritma ini diterapkan. args_traits dibungkus dengan type_identity diteruskan ke lambda secara bergantian. Di dalam lambda, metafunction standar std :: is_base_of digunakan, tetapi karena bisa ada lebih dari satu register, ekspresi pemindaian digunakan untuk mengeksekusinya untuk setiap register sesuai dengan logika OR. Akibatnya, jika ada setidaknya satu argumen yang propertinya tidak mendasar untuk setidaknya satu register, pernyataan static assert akan berfungsi dan menampilkan pesan kesalahan yang jelas. Sangat mudah untuk memahami darimana letak kesalahannya (melewati argumen yang salah ke metode yang set ) dan memperbaikinya.


Implementasi dari algoritma any_of , yang akan dibutuhkan nanti, sangat mirip:


 template <class F, class...Ts> constexpr auto any_of(type_pack<Ts...>, F f) { return (f(type_identity<Ts>{}) or ...); } 

Tugas berikutnya dari algoritma umum adalah untuk menentukan register mana yang perlu ditulis. Untuk melakukan ini, filter daftar register awal dan hanya menyisakan yang ada argumen dalam fungsi kami. Kami membutuhkan algoritma filter yang mengambil type_pack asli, menerapkan fungsi predikat untuk setiap jenis dari daftar, dan menambahkannya ke daftar baru jika predikat mengembalikan true.


 template <class F, class...Ts> constexpr auto filter(type_pack<Ts...>, F f) { auto filter_one = [](auto v, auto f) { using T = typename decltype(v)::type; if constexpr (f(v)) return type_pack<T>{}; else return empty_pack{}; }; return (empty_pack{} + ... + filter_one(type_identity<Ts>{}, f)); } 

Pertama, lambda dijelaskan yang melakukan fungsi predikat pada satu jenis dan mengembalikan type_pack dengan itu jika predikat dikembalikan true, atau kosongkan type_pack jika predikat dikembalikan false . Fitur baru lainnya dari plus terakhir membantu di sini - constexpr if. Esensinya adalah bahwa dalam kode yang dihasilkan hanya ada satu jika cabang, yang kedua dilemparkan. Dan karena tipe yang berbeda kembali di cabang yang berbeda, tanpa constexpr akan ada kesalahan kompilasi. Hasil mengeksekusi lambda ini untuk setiap jenis dari daftar type_pack menjadi satu type_pack dihasilkan, sekali lagi berkat ekspresi type_pack . Tidak ada kelebihan yang cukup dari operator tambahan untuk type_pack . Implementasinya juga cukup sederhana:


 template <class...Ts, class...Us> constexpr auto operator+ (type_pack<Ts...>, type_pack<Us...>) { return type_pack<Ts..., Us...>{}; } 

Menerapkan algoritme baru di atas daftar register, hanya yang harus dituliskan dalam argumen yang ditransfer yang ada di daftar baru.


Algoritma selanjutnya yang akan dibutuhkan adalah foreach . Itu hanya berlaku fungsi untuk setiap jenis dalam daftar, membungkusnya di type_identity . Di sini, operator koma digunakan dalam ekspresi pemindaian, yang melakukan semua tindakan yang dijelaskan oleh koma dan mengembalikan hasil dari tindakan terakhir.


 template <class F, class...Ts> constexpr void foreach(type_pack<Ts...>, F f) { (f(type_identity<Ts>{}), ...); } 

Fungsi ini memungkinkan Anda untuk masuk ke setiap register tempat Anda ingin menulis. Lambda menghitung nilai untuk menulis ke register, menentukan alamat tempat Anda ingin menulis, dan langsung menulis ke register.


Untuk menghitung nilai satu register, nilai untuk setiap argumen yang dimiliki register ini dihitung, dan hasilnya digabungkan dengan OR.


 template<class Register, class...Args> constexpr std::size_t register_value(type_identity<Register> reg, Args...args) { return (arg_value(reg, args) | ...); } 

Perhitungan nilai untuk bidang tertentu harus dilakukan hanya untuk argumen dari mana register ini diwarisi. Untuk argumen tersebut, kami mengekstrak masker dari propertinya, menentukan offset nilai di dalam register dari mask.


 template<class Register, class Arg> constexpr std::size_t arg_value(type_identity<Register>, Arg arg) { constexpr auto arg_traits = traits(type_identity<Arg>{}); //   ,     if constexpr (not std::is_base_of_v<TYPE(arg_traits), Register>) return 0; constexpr auto mask = decltype(arg_traits)::type::mask; constexpr auto arg_shift = shift(mask); return static_cast<std::size_t>(arg) << arg_shift; } 

Anda dapat menulis algoritme untuk menentukan masker offset sendiri, tetapi saya menggunakan fungsi builtin yang ada.


 constexpr auto shift(std::size_t mask) { return __builtin_ffs(mask) - 1; } 

Fungsi terakhir yang menulis nilai ke alamat tertentu tetap ada.


 inline void write(std::size_t address, std::size_t v) { *reinterpret_cast<std::size_t*>(address) |= v; } 

Untuk menguji tugas, tes kecil ditulis:


 // ,    volatile std::size_t arr[3]; int main() { //     ( ) //   ,         auto address = Address{arr}; auto mock_periph = Periph1{address}; //  1      //  3       3 //  4      //     0b00011001 (25) //    0b00000100 (4) mock_periph.set(Enum1::_1, Enum2::_3, Enum3::_4); // all ok // mock_periph.set(Enum4::_0); // must be compilation error } 

Segala sesuatu yang ditulis di sini digabungkan dan dikompilasi menjadi godbolt . Siapa pun di sana dapat bereksperimen dengan pendekatan tersebut. Dapat dilihat bahwa tujuannya terpenuhi: tidak ada akses memori yang tidak perlu. Nilai yang perlu ditulis ke register dihitung pada tahap kompilasi:


 main: mov QWORD PTR Address::value[rip], OFFSET FLAT:arr or QWORD PTR arr[rip], 25 or QWORD PTR arr[rip+8], 4 mov eax, 0 ret 



PS:
Terima kasih kepada semua orang atas komentarnya, terima kasih kepada mereka, saya sedikit mengubah pendekatannya. Anda dapat melihat versi baru di sini


  • menghapus jenis pembantu * _traits, topeng dapat disimpan langsung di daftar.
     enum struct Enum1 { _0, _1, _2, _3, mask = 0b00111 }; 
  • koneksi register dengan argumen sekarang dilakukan bukan melalui warisan, sekarang ini adalah bidang register statis
     static constexpr auto params = type_pack<Enum1, Enum2>{}; 
  • karena koneksi tidak lagi melalui warisan, saya harus menulis fungsi berisi:
     template <class T, class...Ts> constexpr auto contains(type_pack<Ts...>, type_identity<T> v) { return ((type_identity<Ts>{} == v) or ...); } 
  • tanpa jenis yang berlebihan semua makro menghilang
  • Saya meneruskan argumen ke metode melalui parameter template untuk menggunakannya dalam konteks constexpr
  • sekarang dalam metode himpunan logika constexpr jelas dipisahkan dari logika catatan itu sendiri
     template<auto...args> static void set() { constexpr auto values_for_write = extract(registers, args...); for (auto [value, offset] : values_for_write) { write(Address::value + offset, value); } } 
  • Fungsi ekstrak mengalokasikan dalam constexpr array nilai untuk menulis ke register. Implementasinya sangat mirip dengan fungsi set sebelumnya, kecuali bahwa itu tidak langsung menulis ke register.
  • Saya harus menambahkan metafungsi lain yang mengubah type_pack menjadi array sesuai dengan fungsi lambda.
     template <class F, class...Ts> constexpr auto to_array(type_pack<Ts...> pack, F f) { return std::array{f(type_identity<Ts>{})...}; } 

Source: https://habr.com/ru/post/id483378/


All Articles