Menyesali ulang ketidakhadiran dalam C ++ dari statis penuh jika atau ...

... bagaimana cara mengisi kelas templat dengan konten yang berbeda tergantung pada nilai parameter templat?


Sekali waktu, untuk beberapa waktu, bahasa D mulai dibuat sebagai "C ++ yang benar", dengan mempertimbangkan pengalaman yang didapat dalam C ++. Seiring waktu, D telah menjadi bahasa yang tidak kalah rumit dan lebih ekspresif daripada C ++. Dan sudah C ++ mulai memata-matai D. Misalnya, muncul di C ++ 17 if constexpr , menurut pendapat saya, adalah pinjaman langsung dari D, prototipe yang D-shny statis jika .


Sayangnya, if constexpr di C ++ tidak memiliki kekuatan yang sama dengan static if di D. Ada alasan untuk ini , tetapi masih ada kasus ketika Anda hanya bisa menyesal bahwa if constexpr di C ++ tidak memungkinkan Anda untuk mengontrol konten C + + kelas. Saya ingin berbicara tentang salah satu kasus ini.


Kita akan berbicara tentang cara membuat kelas templat, yang isinya (yaitu komposisi metode dan logika beberapa metode) akan berubah tergantung pada parameter apa yang diteruskan ke kelas templat ini. Contoh diambil dari kehidupan nyata, dari pengalaman mengembangkan versi baru dari SObjectizer .


Tugas yang harus diselesaikan


Diperlukan untuk membuat versi pintar dari "smart pointer" untuk menyimpan objek pesan. Sehingga Anda dapat menulis sesuatu seperti:


 message_holder_t<my_message> msg{ new my_message{...} }; send(target, msg); send(another_target, msg); 

Trik kelas message_holder_t ini adalah bahwa ada tiga faktor penting untuk dipertimbangkan.


Apa jenis pesan yang diwarisi dari?


Jenis pesan yang parameterkan message_holder_t dibagi menjadi dua kelompok. Grup pertama adalah pesan yang mewarisi dari tipe dasar message_t . Sebagai contoh:


 struct so5_message final : public so_5::message_t { int a_; std::string b_; std::chrono::milliseconds c_; so5_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} }; 

Dalam hal ini, message_holder_t di dalam dirinya seharusnya hanya berisi pointer ke objek jenis ini. Pointer yang sama harus dikembalikan dalam metode pengambil. Artinya, untuk kasus pewaris dari message_t harus ada sesuatu seperti:


 template<typename M> class message_holder_t { intrusive_ptr_t<M> m_msg; public: ... const M * get() const noexcept { return m_msg.get(); } }; 

Grup kedua adalah pesan tipe pengguna sewenang-wenang yang tidak diwarisi dari message_t . Sebagai contoh:


 struct user_message final { int a_; std::string b_; std::chrono::milliseconds c_; user_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} }; 

Contoh jenis ini di SObjectizer tidak dikirim sendiri, tetapi terlampir dalam pembungkus khusus, user_type_message_t<M> , yang sudah diwarisi dari message_t . Oleh karena itu, untuk tipe seperti itu, message_holder_t harus mengandung sebuah pointer ke user_type_message_t<M> di dalamnya, dan metode pengambil harus mengembalikan sebuah pointer ke M:


 template<typename M> class message_holder_t { intrusive_ptr_t<user_type_message_t<M>> m_msg; public: ... const M * get() const noexcept { return std::addressof(m_msg->m_payload); } }; 

Kekebalan atau mutabilitas pesan


Faktor kedua adalah pembagian pesan menjadi tidak berubah dan bisa berubah. Jika pesan tidak dapat diubah (dan secara default tidak dapat diubah), maka metode pengambil harus mengembalikan pointer konstan ke pesan. Dan jika bisa berubah, maka getter harus mengembalikan pointer yang tidak konstan. Yaitu harus seperti:


 message_holder_t<so5_message> msg1{...}; //  . const int a = msg1->a_; // OK. msg1->a_ = 0; //     ! message_holder_t<mutable_msg<user_message>> msg2{...}; //  . const int a = msg2->a_; // OK. msg2->a_ = 0; // OK. 

shared_ptr vs unique_ptr


Faktor ketiga adalah logika perilaku message_holder_t sebagai smart pointer. Setelah itu harus berperilaku seperti std::shared_ptr , yaitu Anda dapat memiliki beberapa message_holder yang merujuk pada instance pesan yang sama. Dan setelah itu harus berperilaku seperti std::unique_ptr , mis. hanya satu instance message_holder yang dapat merujuk ke instance message.


Secara default, perilaku message_holder_t harus bergantung pada mutabilitas / kekekalan pesan. Yaitu dengan pesan yang tidak dapat diubah, message_holder_t akan berperilaku seperti std::shared_ptr , dan dengan std::unique_ptr dapat berubah seperti std::unique_ptr :


 message_holder_t<so5_message> msg1{...}; message_holder_t<so5_message> msg2 = msg; // OK. message_holder_t<mutable_msg<user_message>> msg3{...}; message_holder_t<mutable_msg<user_message>> msg4 = msg3; // !  ! message_holder_t<mutable_msg<user_message>> msg5 = std::move(msg3); // OK. 

Tetapi hidup adalah hal yang rumit, jadi Anda juga harus dapat secara manual mengatur perilaku message_holder_t . Sehingga Anda dapat membuat message_holder untuk pesan abadi yang berperilaku seperti unique_ptr. Dan agar Anda dapat membuat message_holder untuk pesan yang bisa berubah yang berperilaku seperti shared_ptr:


 using unique_so5_message = so_5::message_holder_t< so5_message, so_5::message_ownership_t::unique>; unique_so5_message msg1{...}; unique_so5_message msg2 = msg1; // !  ! unique_so5_message msg3 = std::move(msg); // OK,   msg3. using shared_user_messsage = so_5::message_holder_t< so_5::mutable_msg<user_message>, so_5::message_ownership_t::shared>; shared_user_message msg4{...}; shared_user_message msg5 = msg4; // OK. 

Dengan demikian, ketika message_holder_t berfungsi seperti shared_ptr, seharusnya memiliki set konstruktor dan operator penugasan yang biasa: keduanya salin dan pindahkan. Selain itu, harus ada metode konstan make_reference , yang mengembalikan salinan pointer yang disimpan di dalam message_holder_t .


Tetapi ketika message_holder_t berfungsi seperti unique_ptr, maka konstruktor dan operator penyalinan harus dilarang untuk itu. Dan metode make_reference harus mengambil pointer dari objek message_holder_t : setelah memanggil make_reference message_holder_t asli harus tetap kosong.


Sedikit lebih formal


Jadi, Anda perlu membuat kelas templat:


 template< typename M, message_ownership_t Ownership = message_ownership_t::autodetected> class message_holder_t {...}; 

yang:


  • intrusive_ptr_t<M> atau intrusive_ptr<user_type_message_t<M>> harus disimpan di dalam, tergantung pada apakah M diwarisi dari message_t ;
  • metode pengambil harus mengembalikan const M* atau M* tergantung pada mutabilitas / keabadian pesan;
  • harus ada satu set lengkap konstruktor dan operator salin / pindahkan, atau hanya operator konstruktor dan pindahkan;
  • Metode make_reference() harus mengembalikan salinan intrusive_ptr yang disimpan, atau harus mengambil nilai intrusive_ptr dan membiarkan message_holder_t asli kosong. Dalam kasus pertama, make_reference() harus konstan, pada metode kedua - tidak konstan.

Dua item terakhir dari daftar ditentukan oleh parameter Kepemilikan (serta mutabilitas pesan, jika autodetected digunakan untuk Kepemilikan).


Bagaimana diputuskan


Pada bagian ini, kami akan mempertimbangkan semua komponen yang membentuk solusi akhir. Nah, solusi yang dihasilkan itu sendiri. Fragmen kode yang dihapus dari semua detail yang mengganggu akan ditampilkan. Jika seseorang tertarik pada kode asli, maka Anda dapat melihatnya di sini .


Penafian


Solusi yang ditunjukkan di bawah ini tidak berpura-pura cantik, ideal atau panutan. Itu ditemukan, diimplementasikan, diuji dan didokumentasikan dalam waktu singkat, di bawah tekanan dari tenggat waktu. Mungkin jika ada lebih banyak waktu, dan lebih banyak terlibat dalam mencari solusi muda masuk akal dan berpengetahuan luas dalam pengembang C ++ modern, itu akan berubah menjadi lebih kompak, lebih sederhana dan lebih mudah dimengerti. Tapi, ternyata, itu terjadi ... "Jangan tembak pianis", secara umum.


Urutan langkah-langkah dan keajaiban template yang sudah jadi


Jadi, kita perlu memiliki kelas dengan beberapa set metode. Isi kit ini harus berasal dari suatu tempat. Dari mana?


Dalam D, kita dapat menggunakan static if dan mendefinisikan bagian kelas yang berbeda tergantung pada kondisi yang berbeda. Di beberapa Ruby, kita bisa mencampur metode ke dalam kelas kita menggunakan metode include . Tetapi kita berada di C ++, di mana sejauh ini kemungkinan kita sangat terbatas: kita dapat mendefinisikan metode / atribut langsung di dalam kelas, atau kita dapat mewarisi metode / atribut dari beberapa kelas dasar.


Kami tidak dapat mendefinisikan metode / atribut yang berbeda di dalam kelas tergantung pada beberapa kondisi, karena C ++ if constexpr bukan static if D static if . Akibatnya, hanya warisan yang tersisa.


Pembaruan. Seperti yang disarankan dalam komentar, saya harus berbicara lebih hati-hati di sini. Karena C ++ memiliki SFINAE, kami dapat mengaktifkan / menonaktifkan visibilitas metode individual di kelas melalui SFINAE (mis., Mencapai efek yang mirip dengan static if ). Tetapi pendekatan ini memiliki dua kelemahan serius, menurut saya,. Pertama, jika metode seperti itu tidak 1-2-3, tetapi 4-5 atau lebih, itu membosankan untuk memformat masing-masing menggunakan SFINAE, dan ini mempengaruhi keterbacaan kode. Kedua, SFINAE tidak membantu kami menambah / menghapus atribut kelas (bidang).

Di C ++, kita bisa mendefinisikan beberapa kelas dasar yang darinya kita akan mewarisi message_holder_t . Dan pilihan dari satu atau beberapa kelas dasar akan sudah dilakukan tergantung pada nilai-nilai parameter templat, menggunakan std :: conditional .


Tetapi triknya adalah bahwa kita tidak perlu hanya satu set kelas dasar, tetapi rantai kecil warisan. Pada awalnya akan ada kelas yang akan menentukan fungsi umum yang akan diperlukan dalam hal apa pun. Berikutnya adalah kelas dasar yang akan menentukan logika perilaku "penunjuk pintar". Dan kemudian akan ada kelas yang menentukan getter yang diperlukan. Dalam urutan ini kami akan mempertimbangkan kelas yang diimplementasikan.


Tugas kita disederhanakan oleh fakta bahwa SObjectizer sudah memiliki templat templat siap pakai yang menentukan apakah pesan diwarisi dari message_t , serta sarana untuk memeriksa mutabilitas pesan . Oleh karena itu, dalam implementasinya, kita hanya akan menggunakan sihir yang sudah jadi ini dan tidak akan membahas detail pekerjaannya.


Basis penyimpanan pointer umum


Mari kita mulai dengan tipe basis umum yang menyimpan intrusive_ptr yang sesuai, dan juga menyediakan serangkaian metode umum yang dibutuhkan oleh implementasi message_holder_t :


 template< typename Payload, typename Envelope > class basic_message_holder_impl_t { protected : intrusive_ptr_t< Envelope > m_msg; public : using payload_type = Payload; using envelope_type = Envelope; basic_message_holder_impl_t() noexcept = default; basic_message_holder_impl_t( intrusive_ptr_t< Envelope > msg ) noexcept : m_msg{ std::move(msg) } {} void reset() noexcept { m_msg.reset(); } [[nodiscard]] bool empty() const noexcept { return static_cast<bool>( m_msg ); } [[nodiscard]] operator bool() const noexcept { return !this->empty(); } [[nodiscard]] bool operator!() const noexcept { return this->empty(); } }; 

Kelas templat ini memiliki dua parameter. Yang pertama, Payload, menetapkan tipe yang harus digunakan oleh metode pengambil. Sedangkan yang kedua, Envelope, menetapkan tipe untuk intrusive_ptr. Dalam kasus ketika jenis pesan diwarisi dari message_t kedua parameter ini akan memiliki nilai yang sama. Tetapi jika pesan tersebut tidak diwarisi dari message_t , maka jenis pesan akan digunakan sebagai Payload, dan user_type_message_t<Payload> akan user_type_message_t<Payload> sebagai Envelope.


Saya pikir pada dasarnya konten kelas ini tidak menimbulkan pertanyaan. Tetapi dua hal harus dicatat secara terpisah.


Pertama, penunjuk itu sendiri, mis. atribut m_msg didefinisikan di bagian yang dilindungi sehingga kelas-kelas pewaris dapat mengaksesnya.


Kedua, untuk kelas ini, kompiler itu sendiri menghasilkan semua konstruktor yang diperlukan dan operator salin / pindahkan. Dan pada level kelas ini, kami belum melarang apa pun.


Pisahkan basis untuk perilaku shared_ptr dan unique_ptr


Jadi, kami memiliki kelas yang menyimpan pointer ke pesan. Sekarang kita dapat mendefinisikan pewarisnya, yang akan berlaku sebagai shared_ptr atau sebagai unique_ptr.


Mari kita mulai dengan kasus perilaku shared_ptr, karena di sini adalah kode paling sedikit:


 template< typename Payload, typename Envelope > class shared_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() const noexcept { return this->m_msg; } }; 

Tidak ada yang rumit: mewarisi dari basic_message_holder_impl_t , mewarisi semua konstruktornya, dan mendefinisikan implementasi sederhana, non-destruktif dari make_reference() .


Untuk kasus unique_ptr-behaviour, kodenya lebih besar, meskipun tidak ada yang rumit di dalamnya:


 template< typename Payload, typename Envelope > class unique_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; unique_message_holder_impl_t( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t( unique_message_holder_impl_t && ) = default; unique_message_holder_impl_t & operator=( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t & operator=( unique_message_holder_impl_t && ) = default; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() noexcept { return { std::move(this->m_msg) }; } }; 

Sekali lagi, kita mewarisi dari basic_message_holder_impl_t dan mewarisi konstruktor yang kita butuhkan darinya (ini adalah konstruktor default dan konstruktor inisialisasi). Tetapi pada saat yang sama, kami mendefinisikan konstruktor dan menyalin / memindahkan operator sesuai dengan logika unique_ptr: kami melarang menyalin, kami menerapkan langkah tersebut.


Kami juga memiliki metode make_reference() destruktif di make_reference() .


Itu saja, sebenarnya. Tetap hanya untuk mewujudkan pilihan antara dua kelas dasar ini ...


Memilih antara perilaku shared_ptr dan unique_ptr


Untuk memilih antara perilaku shared_ptr dan unique_ptr, Anda memerlukan metafungsi berikut (metafungsi karena "berfungsi" dengan tipe dalam waktu kompilasi):


 template< typename Msg, message_ownership_t Ownership > struct impl_selector { static_assert( !is_signal<Msg>::value, "Signals can't be used with message_holder" ); using P = typename message_payload_type< Msg >::payload_type; using E = typename message_payload_type< Msg >::envelope_type; using type = std::conditional_t< message_ownership_t::autodetected == Ownership, std::conditional_t< message_mutability_t::immutable_message == message_mutability_traits<Msg>::mutability, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> >, std::conditional_t< message_ownership_t::shared == Ownership, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> > >; }; 

Metafungsi ini menerima kedua parameter dari daftar parameter message_holder_t dan, sebagai akibatnya (yaitu, definisi type bersarang), "mengembalikan" tipe dari mana ia harus diwarisi. Yaitu baik shared_message_holder_impl_t atau unique_message_holder_impl_t .


Di dalam definisi impl_selector Anda dapat melihat jejak sihir yang disebutkan di atas, dan yang tidak kami masuki: message_payload_type<Msg>::payload_type , message_payload_type<Msg>::envelope_type dan message_mutability_traits<Msg>::mutability .


Dan untuk menggunakan impl_selector lebih mudah, maka kita akan mendefinisikan nama yang lebih pendek untuk itu:


 template< typename Msg, message_ownership_t Ownership > using impl_selector_t = typename impl_selector<Msg, Ownership>::type; 

Basis untuk getter


Jadi, kita sudah memiliki kesempatan untuk memilih basis yang berisi pointer dan mendefinisikan perilaku "smart pointer". Sekarang kita perlu menyediakan pangkalan ini dengan metode pengambil. Mengapa kita membutuhkan satu kelas sederhana:


 template< typename Base, typename Return_Type > class msg_accessors_t : public Base { public : using Base::Base; [[nodiscard]] Return_Type * get() const noexcept { return get_ptr( this->m_msg ); } [[nodiscard]] Return_Type & operator * () const noexcept { return *get(); } [[nodiscard]] Return_Type * operator->() const noexcept { return get(); } }; 

Ini adalah kelas templat yang bergantung pada dua parameter, tetapi artinya sangat berbeda. Parameter Base akan menjadi hasil impl_selector impl_selector yang ditunjukkan di atas. Yaitu sebagai parameter Base, kelas dasar diatur untuk mewarisi.


Penting untuk dicatat bahwa jika pewarisan berasal dari unique_message_holder_impl_t , di mana konstruktor dan operator penyalinan dilarang, maka kompiler tidak akan dapat membuat konstruktor dan menyalin operator untuk msg_accessors_t . Itulah yang kita butuhkan.


Jenis pesan, penunjuk / tautan yang akan dikembalikan oleh getter, akan bertindak sebagai parameter Return_Type. Caranya adalah bahwa untuk pesan yang tidak dapat diubah dari tipe Msg parameter Return_Type akan ditetapkan ke const Msg . Sedangkan untuk pesan yang dapat diubah dari tipe Msg parameter Return_Type akan memiliki nilai Msg . Dengan demikian, metode get() akan mengembalikan const Msg* untuk pesan yang tidak dapat diubah, dan hanya Msg* untuk pesan yang bisa diubah.


Menggunakan fungsi get_ptr() gratis get_ptr() masalah bekerja dengan pesan yang tidak diwarisi dari message_t diselesaikan:


 template< typename M > M * get_ptr( const intrusive_ptr_t<M> & msg ) noexcept { return msg.get(); } template< typename M > M * get_ptr( const intrusive_ptr_t< user_type_message_t<M> > & msg ) noexcept { return std::addressof(msg->m_payload); } 

Yaitu jika pesan tidak diwarisi dari message_t dan disimpan sebagai user_type_message_t<Msg> , maka kelebihan kedua disebut. Dan jika itu diwariskan, maka yang pertama kelebihan.


Memilih basis spesifik untuk getter


Jadi, templat msg_accessors_t membutuhkan dua parameter. Yang pertama dihitung dengan impl_selector impl_selector. Tetapi untuk membentuk tipe basis spesifik dari msg_accessors_t , kita perlu menentukan nilai parameter kedua. Satu lagi metafungsi dimaksudkan untuk ini:


 template< message_mutability_t Mutability, typename Base > struct accessor_selector { using type = std::conditional_t< message_mutability_t::immutable_message == Mutability, msg_accessors_t<Base, typename Base::payload_type const>, msg_accessors_t<Base, typename Base::payload_type> >; }; 

Anda hanya dapat memperhatikan perhitungan parameter Return_Type. Salah satu dari beberapa kasus di mana const timur berguna;)


Nah, untuk meningkatkan keterbacaan kode berikut, opsi yang lebih ringkas untuk bekerja dengannya:


 template< message_mutability_t Mutability, typename Base > using accessor_selector_t = typename accessor_selector<Mutability, Base>::type; 

Message_holder_t penerus akhir


Sekarang Anda dapat melihat apa itu message_holder_t , untuk implementasi yang diperlukan semua kelas dasar dan metafunctions (bagian dari metode untuk membangun sebuah instance dari pesan yang disimpan di message_holder dihapus dari implementasi):


 template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t : public details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> > { using base_type = details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >; public : using payload_type = typename base_type::payload_type; using envelope_type = typename base_type::envelope_type; using base_type::base_type; friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.message_reference(), b.message_reference() ); } }; 

Faktanya, semua yang kami analisis di atas diperlukan untuk merekam "panggilan" dari dua metafungsi ini:


 details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> > 

Karena ini bukan pilihan pertama, tetapi hasil dari penyederhanaan dan pengurangan kode, saya dapat mengatakan bahwa bentuk-bentuk metafungsi yang kompak sangat mengurangi jumlah kode dan meningkatkan kelengkapannya (jika secara umum pantas untuk berbicara tentang kelayakan di sini).


Dan apa yang akan terjadi jika ...


Tetapi jika dalam C ++ if constexpr sama kuatnya dengan static if dalam D, maka Anda dapat menulis sesuatu seperti:


Versi hipotetis dengan lebih maju jika constexpr
 template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t { static constexpr const message_mutability_t Mutability = details::message_mutability_traits<Msg>::mutability; static constexpr const message_ownership_t Actual_Ownership = (message_ownership_t::unique == Ownership || (message_mutability_t::mutable_msg == Mutability && message_ownership_t::autodetected == Ownership)) ? message_ownership_t::unique : message_ownership_t::shared; public : using payload_type = typename message_payload_type< Msg >::payload_type; using envelope_type = typename message_payload_type< Msg >::envelope_type; private : using getter_return_type = std::conditional_t< message_mutability_t::immutable_msg == Mutability, payload_type const, payload_type >; public : message_holder_t() noexcept = default; message_holder_t( intrusive_ptr_t< envelope_type > mf ) noexcept : m_msg{ std::move(mf) } {} if constexpr(message_ownership_t::unique == Actual_Ownership ) { message_holder_t( const message_holder_t & ) = delete; message_holder_t( message_holder_t && ) noexcept = default; message_holder_t & operator=( const message_holder_t & ) = delete; message_holder_t & operator=( message_holder_t && ) noexcept = default; } friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.m_msg, b.m_msg ); } [[nodiscard]] getter_return_type * get() const noexcept { return get_const_ptr( m_msg ); } [[nodiscard]] getter_return_type & operator * () const noexcept { return *get(); } [[nodiscard]] getter_return_type * operator->() const noexcept { return get(); } if constexpr(message_ownership_t::shared == Actual_Ownership) { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() const noexcept { return m_msg; } } else { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() noexcept { return { std::move(m_msg) }; } } private : intrusive_ptr_t< envelope_type > m_msg; }; 

, . C++ :(
( C++ "" ).


, , ++. , , , . , message_holder_t . , , if constexpr .


Kesimpulan


, C++. , . , , .


, .


, , ++ , . , . , , . , . C++98/03 , C++11 .

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


All Articles