Baru-baru ini, ketika mengerjakan versi baru dari SObjectizer , saya dihadapkan dengan tugas mengendalikan tindakan pengembang dalam waktu kompilasi. Intinya adalah bahwa sebelumnya seorang programmer dapat membuat panggilan formulir:
receive(from(ch).empty_timeout(150ms), ...); receive(from(ch).handle_n(2).no_wait_on_empty(), ...); receive(from(ch).empty_timeout(2s).extract_n(20).stop_on(...), ...); receive(from(ch).no_wait_on_empty().stop_on(...), ...);
Operasi accept () memerlukan seperangkat parameter, di mana rangkaian metode digunakan, seperti yang ditunjukkan di atas from(ch).empty_timeout(150ms)
atau from(ch).handle_n(2).no_wait_on_empty()
. Pada saat yang sama, memanggil metode handle_n () / extract_n (), yang membatasi jumlah pesan yang akan diekstraksi / diproses, adalah opsional. Karena itu, semua rantai yang ditunjukkan di atas adalah benar.
Tetapi dalam versi baru, itu diperlukan untuk memaksa pengguna untuk secara eksplisit menunjukkan jumlah pesan untuk diekstraksi dan / atau diproses. Yaitu rantai formulir from(ch).empty_timeout(150ms)
sekarang menjadi salah. Itu harus diganti dengan from(ch).handle_all().empty_timeout(150ms)
.
Dan saya ingin membuatnya sehingga kompiler akan mengalahkan programmer dengan tangan jika programmer lupa membuat panggilan ke handle_all (), handle_n () atau extract_n ().
Bisakah C ++ membantu dengan ini?
Ya Dan jika seseorang tertarik dengan cara tepatnya, maka Anda dipersilakan di bawah kucing.
Ada lebih dari fungsi accept ()
Fungsi accept () ditunjukkan di atas, parameter yang ditetapkan menggunakan rantai panggilan (juga dikenal sebagai pola pembangun ). Tetapi ada juga fungsi select (), yang menerima parameter yang hampir sama:
select(from_all().empty_timeout(150ms), case_(...), case_(...), ...); select(from_all().handle_n(2).no_wait_on_empty(), case_(...), case_(...), ...); select(from_all().empty_timeout(2s).extract_n(20).stop_on(...), case_(...), case_(...), ...); select(from_all().no_wait_on_empty().stop_on(...), case_(...), case_(...), ...);
Karenanya, saya ingin mendapatkan satu solusi yang cocok untuk select () dan accept (). Selain itu, parameter untuk select () dan accept () sendiri sudah diwakili dalam kode untuk menghindari copy-and-paste. Tetapi ini akan dibahas di bawah.
Kemungkinan solusi
Jadi, tugasnya adalah agar pengguna memanggil handle_all (), handle_n () atau extract_n () tanpa gagal.
Pada prinsipnya, ini dapat dicapai tanpa menggunakan keputusan yang rumit. Misalnya, Anda bisa memasukkan argumen tambahan untuk select () dan menerima ():
receive(handle_all(), from(ch).empty_timeout(150ms), ...); select(handle_n(20), from_all().no_wait_on_empty(), ...);
Atau mungkin akan memaksa pengguna untuk membuat panggilan accept () / select () berbeda:
receive(handle_all(from(ch).empty_timeout(150ms)), ...); select(handle_n(20, from_all().no_wait_on_empty()), ...);
Tetapi masalahnya di sini adalah bahwa ketika beralih ke versi baru dari SObjectizer, pengguna harus mengulang kodenya. Bahkan jika kode tersebut, pada prinsipnya, tidak memerlukan pengerjaan ulang. Katakan, dalam situasi ini:
receive(from(ch).handle_n(2).no_wait_on_empty(), ...); select(from_all().empty_timeout(2s).extract_n(20).stop_on(...), case_(...), case_(...), ...);
Dan ini, menurut saya, adalah masalah yang sangat serius. Yang membuat Anda mencari cara lain. Dan metode ini akan dijelaskan di bawah ini.
Jadi di mana CRTP masuk?
Judul artikel menyebutkan CRTP. Dia juga Curiously Recurring Template Pattern (mereka yang ingin berkenalan dengan teknik yang menarik, tetapi sedikit toleran otak ini, dapat mulai dengan serangkaian posting ini di blog Fluent C ++).
CRTP disebutkan karena melalui CRTP kami mengimplementasikan pekerjaan dengan parameter fungsi accept () dan select (). Karena bagian terbesar dari parameter untuk menerima () dan pilih () adalah sama, kode menggunakan sesuatu seperti ini:
template<typename Derived> class bulk_processing_params_t { ...;
Mengapa CRTP ada di sini?
Kami harus menggunakan CRTP di sini sehingga metode setter yang didefinisikan dalam kelas dasar dapat mengembalikan referensi bukan ke tipe dasar, tetapi ke yang diturunkan.
Artinya, jika bukan CRTP yang digunakan, tetapi warisan biasa, maka kita hanya bisa menulis seperti ini:
class bulk_processing_params_t { public:
Tetapi mekanisme primitif seperti itu tidak akan memungkinkan kita untuk menggunakan pola pembangun yang sama, karena:
receive_processing_params_t{}.handle_n(20).receive_payload(0)
tidak dikompilasi. Metode handle_n () akan mengembalikan referensi ke bulk_processing_params_t, dan di sana metode accept_payload () belum didefinisikan.
Tetapi dengan CRTP kami tidak memiliki masalah dengan pola pembangun.
Keputusan akhir
Solusi terakhir adalah untuk tipe final, seperti accept_processing_params_t dan select_processing_params_t, untuk menjadi tipe templat sendiri. Sehingga mereka diparameterisasi dengan skalar dari bentuk berikut:
enum class msg_count_status_t { undefined, defined };
Dan sehingga tipe terakhir dapat dikonversi dari T <msg_count_status_t :: undefined> ke T <msg_count_status_t :: defined>.
Ini akan memungkinkan, misalnya, dalam fungsi accept () untuk menerima accept_processing_params_t dan memeriksa nilai Status dalam waktu-comp. Sesuatu seperti:
template< msg_count_status_t Msg_Count_Status, typename... Handlers > inline mchain_receive_result_t receive( const mchain_receive_params_t<Msg_Count_Status> & params, Handlers &&... handlers ) { static_assert( Msg_Count_Status == msg_count_status_t::defined, "message count to be processed/extracted should be defined " "by using handle_all()/handle_n()/extract_n() methods" );
Secara umum, semuanya sederhana, seperti biasa: ambil dan lakukan;)
Deskripsi keputusan yang dibuat
Mari kita lihat contoh minimal, terlepas dari spesifikasi SObjectizer, seperti yang terlihat.
Jadi, kami sudah memiliki tipe yang menentukan apakah batas jumlah pesan diatur atau tidak ditetapkan:
enum class msg_count_status_t { undefined, defined };
Selanjutnya, kita membutuhkan struktur di mana semua parameter umum akan disimpan:
struct basic_data_t { int to_extract_{}; int to_handle_{}; int common_payload_{}; };
Secara umum tidak masalah apa isi basic_data_t nantinya. Misalnya, set minimal bidang yang ditunjukkan di atas cocok.
Sehubungan dengan basic_data_t, penting untuk operasi tertentu (apakah itu menerima (), pilih (), atau sesuatu yang lain), jenis konkretnya sendiri akan dibuat yang mewarisi basic_data_t. Misalnya, untuk menerima () dalam contoh abstrak kami, ini akan menjadi struktur berikut:
struct receive_specific_data_t final : public basic_data_t { int receive_payload_{}; receive_specific_data_t() = default; receive_specific_data_t(int v) : receive_payload_{v} {} };
Kami berasumsi bahwa struktur basic_data_t dan turunannya tidak menyebabkan kesulitan. Oleh karena itu, kami beralih ke bagian solusi yang lebih kompleks.
Sekarang kita membutuhkan pembungkus di sekitar basic_data_t, yang akan menyediakan metode pengambil. Ini akan menjadi kelas templat dari formulir berikut:
template<typename Basic_Data> class basic_data_holder_t { private : Basic_Data data_; protected : void set_to_extract(int v) { data_.to_extract_ = v; } void set_to_handle(int v) { data_.to_handle_ = v; } void set_common_payload(int v) { data_.common_payload_ = v; } const auto & data() const { return data_; } public : basic_data_holder_t() = default; basic_data_holder_t(Basic_Data data) : data_{std::move(data)} {} int to_extract() const { return data_.to_extract_; } int to_handle() const { return data_.to_handle_; } int common_payload() const { return data_.common_payload_; } };
Kelas ini adalah boilerplate sehingga dapat berisi pewaris apa pun dari basic_data_t, meskipun mengimplementasikan metode pengambil hanya untuk bidang yang ada di basic_data_t.
Sebelum kita beralih ke bagian yang lebih kompleks dari solusi, Anda harus memperhatikan metode data () di basic_data_holder_t. Ini adalah metode yang penting dan kita akan bertemu nanti.
Sekarang kita bisa beralih ke kelas templat kunci, yang bisa terlihat sangat menakutkan bagi orang-orang yang tidak terlalu berdedikasi untuk C ++ modern:
template<typename Data, typename Derived> class basic_params_t : public basic_data_holder_t<Data> { using base_type = basic_data_holder_t<Data>; public : using actual_type = Derived; using data_type = Data; protected : actual_type & self_reference() { return static_cast<actual_type &>(*this); } decltype(auto) clone_as_defined() { return self_reference().template clone_if_necessary< msg_count_status_t::defined >(); } public : basic_params_t() = default; basic_params_t(data_type data) : base_type{std::move(data)} {} decltype(auto) handle_all() { this->set_to_handle(0); return clone_as_defined(); } decltype(auto) handle_n(int v) { this->set_to_handle(v); return clone_as_defined(); } decltype(auto) extract_n(int v) { this->set_to_extract(v); return clone_as_defined(); } actual_type & common_payload(int v) { this->set_common_payload(v); return self_reference(); } using base_type::common_payload; };
Basic_params_t ini adalah templat CRTP utama. Hanya sekarang ini diparameterisasi oleh dua parameter.
Parameter pertama adalah tipe data yang harus terkandung di dalamnya. Misalnya, terima_specific_data_t atau pilih_specific_data_t.
Parameter kedua adalah jenis penerus yang akrab dengan CRTP. Ini digunakan dalam metode self_reference () untuk mendapatkan referensi ke tipe turunan.
Titik kunci dalam implementasi templat basic_params_t adalah metode clone_as_defined (). Metode ini mengharapkan pewaris untuk menerapkan metode clone_if_n Diperlukan (). Dan clone_if_n diperlukan () ini hanya dirancang untuk mengubah objek T <msg_count_status_t :: undefined> menjadi objek T <msg_count_status_t :: defined>. Dan transformasi semacam itu dimulai dalam metode setter handle_all (), handle_n () dan extract_n ().
Selain itu, Anda dapat memperhatikan fakta bahwa clone_as_defined (), handle_all (), handle_n () dan extract_n () menentukan jenis nilai pengembaliannya sebagai decltype (otomatis). Ini adalah trik lain, yang akan kita bicarakan segera.
Sekarang kita sudah dapat melihat salah satu dari jenis terakhir, yang semua ini dikandung:
template< msg_count_status_t Msg_Count_Status > class receive_specific_params_t final : public basic_params_t< receive_specific_data_t, receive_specific_params_t<Msg_Count_Status> > { using base_type = basic_params_t< receive_specific_data_t, receive_specific_params_t<Msg_Count_Status> >; public : template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status != Msg_Count_Status, receive_specific_params_t<New_Msg_Count_Status> > clone_if_necessary() const { return { this->data() }; } template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status == Msg_Count_Status, receive_specific_params_t& > clone_if_necessary() { return *this; } receive_specific_params_t(int receive_payload) : base_type{ typename base_type::data_type{receive_payload} } {} receive_specific_params_t(typename base_type::data_type data) : base_type{ std::move(data) } {} int receive_payload() const { return this->data().receive_payload_; } };
Hal pertama yang harus Anda perhatikan di sini adalah konstruktor, yang mengambil base_type :: data_type. Menggunakan konstruktor ini, nilai-nilai parameter saat ini ditransfer selama transformasi dari T <msg_count_status_t :: undefined> ke T <msg_count_status_t :: defined>.
Secara umum, accept_specific_params_t ini kira-kira seperti ini:
template<typename V, int K> class holder_t { V v_; public: holder_t() = default; holder_t(V v) : v_{std::move(v)} {} const V & value() const { return v_; } }; holder_t<std::string, 0> v1{"Hello!"}; holder_t<std::string, 1> v2; v2 = v1;
Dan hanya konstruktor di atas accept_specific_params_t memungkinkan Anda untuk menginisialisasi accept_specific_params_t <msg_count_status_t :: defined> dengan nilai dari accept_specific_params_t <msg_count_status_t :: undefined>.
Hal penting kedua dalam accept_specific_params_t adalah dua metode clone_if_n Diperlukan ().
Kenapa ada dua? Dan apa arti semua sihir SFINAE-vskaya ini dalam definisi mereka?
Dua metode clone_if_n Diperlukan () telah dibuat untuk menghindari transformasi yang tidak perlu. Misalkan seorang programmer disebut metode handle_n () dan sudah menerima accept_specific_params_t <msg_count_status_t :: defined>. Dan itu disebut extract_n (). Ini diizinkan, handle_n () dan extract_n () mengatur batasan yang sedikit berbeda. Panggilan ke extract_n () juga harus memberi kita accept_specific_params_t <msg_count_status_t :: defined>. Tapi kita sudah punya satu. Jadi mengapa tidak menggunakan kembali yang sudah ada?
Itu sebabnya ada dua metode clone_if_n Diperlukan () di sini. Yang pertama akan bekerja ketika transformasi sangat dibutuhkan:
template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status != Msg_Count_Status, receive_specific_params_t<New_Msg_Count_Status> > clone_if_necessary() const { return { this->data() }; }
Kompiler akan memilihnya, misalnya, ketika status berubah dari tidak terdefinisi menjadi ditentukan. Dan metode ini akan mengembalikan objek baru. Dan ya, dalam penerapan metode ini, kami memperhatikan panggilan data (), yang sudah didefinisikan di basic_data_holder_t.
Metode kedua:
template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status == Msg_Count_Status, receive_specific_params_t& > clone_if_necessary() { return *this; }
akan dipanggil ketika tidak perlu mengubah status. Dan metode ini mengembalikan referensi ke objek yang ada.
Sekarang harus menjadi jelas mengapa di basic_params_t untuk sejumlah metode, tipe pengembalian didefinisikan sebagai jenis jenis (otomatis). Lagi pula, metode ini bergantung pada versi clone_if_n diperlukan () mana yang akan dipanggil dalam tipe turunan, dan di sana objek atau tautan dapat dikembalikan ... Anda tidak dapat memprediksi sebelumnya. Dan di sini decltype (otomatis) datang untuk menyelamatkan.
Penafian kecil
Contoh minimalis yang dideskripsikan ditujukan pada demonstrasi paling sederhana dan paling dapat dipahami dari solusi yang dipilih. Oleh karena itu, tidak ada hal-hal yang cukup jelas yang memohon untuk dimasukkan dalam kode.
Misalnya, metode basic_data_holder_t :: data () mengembalikan referensi konstan ke data. Ini mengarah ke penyalinan nilai parameter selama transformasi T <msg_count_status_t :: undefined> ke T <msg_count_status_t :: defined>. Jika menyalin parameter adalah operasi yang mahal, maka Anda harus bingung dengan memindahkan semantik dan metode data () dapat memiliki formulir berikut:
auto data() { return std::move(data_); }
Juga sekarang, di setiap tipe final (seperti accept_specific_params_t dan select_specific_params_t), Anda harus menyertakan implementasi metode clone_if_n diperlukan. Yaitu di tempat ini kami masih menggunakan copy paste. Mungkin juga harus ada sesuatu yang muncul untuk menghindari duplikasi dari jenis kode yang sama.
Yah dan ya, noexcept tidak dimasukkan ke dalam kode untuk mengurangi "sintaks overhead" (s).
Itu semua
Kode sumber untuk contoh minimalis yang dibahas di sini dapat ditemukan di sini . Dan Anda dapat bermain di kompiler online, misalnya, di sini (Anda dapat mengomentari panggilan ke handle_all () pada baris 163 dan melihat apa yang terjadi).
Saya tidak ingin mengatakan bahwa pendekatan yang saya terapkan adalah satu-satunya yang benar. Tapi, pertama, saya melihat alternatif kecuali di copy-paste. Dan, kedua, sama sekali tidak sulit untuk melakukan ini, dan untungnya, tidak butuh banyak waktu. Tetapi pukulan kompiler sangat membantu segera, karena pengujian dan contoh lama disesuaikan dengan fitur baru dari versi terbaru dari SObjectizer.
Jadi, bagi saya, C ++ sekali lagi menegaskan bahwa itu rumit. Namun tidak hanya begitu saja, tetapi untuk memberi lebih banyak peluang kepada pengembang. Yah, saya tidak akan terkejut jika semua ini dapat diperoleh dalam C ++ modern dengan cara yang bahkan lebih sederhana daripada saya.
PS. Jika salah satu pembaca mengikuti SObjectizer, maka saya dapat mengatakan bahwa versi baru 5.6, di mana kompatibilitas dengan cabang 5,5 dilanggar secara signifikan, sudah bernafas sedikit. Anda dapat menemukannya di BitBucket . Rilis ini masih jauh, tetapi SObjectizer-5.6 sudah seperti yang seharusnya. Anda dapat mengambil, mencoba, dan membagikan kesan Anda.