Pendekatan saya untuk mengimplementasikan delegasi di C ++: memanggil fungsi dengan parameter yang tidak diketahui saat runtime

Latar belakang


Saya suka bahasa C ++. Saya bahkan akan mengatakan bahwa ini adalah bahasa favorit saya. Selain itu, saya menggunakan teknologi .NET untuk pengembangan saya, dan banyak ide di dalamnya, menurut saya, sangat menakjubkan. Setelah saya datang dengan ide - bagaimana menerapkan beberapa cara refleksi dan panggilan fungsi dinamis di C ++? Saya benar-benar ingin C ++ memiliki keunggulan CLI seperti memanggil delegasi dengan jumlah parameter dan tipe yang tidak diketahui. Ini bisa bermanfaat, misalnya, ketika tidak diketahui sebelumnya tipe data apa yang perlu dipanggil.

Tentu saja, peniruan delegasi yang lengkap terlalu rumit, sehingga artikel ini hanya akan menunjukkan arsitektur umum perpustakaan dan solusi untuk beberapa masalah penting yang muncul ketika berhadapan dengan apa yang tidak secara langsung didukung oleh bahasa.

Fungsi panggilan dengan jumlah parameter yang tidak terbatas dan tipe yang tidak dikenal selama kompilasi


Tentu saja, ini adalah masalah utama dengan C ++, yang tidak mudah dipecahkan. Tentu saja, di C ++ ada alat yang diwarisi dari C - varargs , dan kemungkinan besar ini adalah hal pertama yang terlintas dalam pikiran ... Namun, mereka tidak cocok, pertama, karena sifatnya yang tidak aman (seperti banyak hal dari C), kedua, ketika menggunakan argumen seperti itu, Anda harus tahu sebelumnya apa jenis argumen itu. Namun, hampir pasti, ini tidak semua masalah dengan varargs . Secara umum, alat ini bukan asisten di sini.

Dan sekarang saya akan membuat daftar alat yang membantu saya memecahkan masalah ini.

std :: any


Dimulai dengan C ++ 17, bahasa ini memiliki wadah penampung yang bagus untuk apa saja - beberapa kesamaan jauh dengan System.Object di CLI adalah std :: any . Wadah ini benar-benar dapat menyimpan apa pun, dan bahkan caranya: efisien! - standar merekomendasikan agar Anda menyimpan objek kecil langsung di dalamnya, objek besar sudah dapat disimpan dalam memori dinamis (walaupun perilaku ini tidak wajib, Microsoft melakukan ini dalam implementasi C ++, yang merupakan berita baik). Dan hanya itu yang bisa disebut kesamaan karena System.Object terlibat dalam hubungan pewarisan ("is a"), dan std :: any terlibat dalam hubungan keanggotaan ("memiliki"). Selain data, wadah berisi pointer ke objek std :: type_info - RTTI tentang jenis yang objeknya "berbaring" di wadah.

Seluruh file header <any> dialokasikan untuk wadah.

Untuk "menarik" objek dari wadah, Anda harus menggunakan fungsi template std :: any_cast () , yang mengembalikan referensi ke objek.
Contoh penggunaan:

#include <any> void any_test() { std::any obj = 5; int from_any = std::any_cast<int>(obj); } 

Jika jenis yang diminta tidak cocok dengan apa yang dimiliki objek di dalam wadah, maka pengecualian std :: bad_any_cast dilemparkan .

Selain kelas std :: any , std :: bad_any_cast dan fungsi std :: any_cast , dalam file header ada fungsi template std :: make_any mirip dengan std :: make_share , std :: make_pair dan fungsi lain dari jenis ini.

RTTI


Tentu saja, praktis tidak realistis dalam C ++ untuk mengimplementasikan pemanggilan fungsi dinamis tanpa mengetikkan informasi saat runtime. Setelah semua, perlu untuk entah bagaimana memeriksa apakah jenis yang benar sudah lulus atau tidak.

Dukungan RTTI primitif dalam C ++ telah ada selama beberapa waktu. Itu intinya, itu primitif - kita bisa belajar sedikit tentang suatu tipe, kecuali nama-nama yang didekorasi dan tidak didekorasi. Selain itu, kita dapat membandingkan jenis satu sama lain.

Biasanya, istilah "RTTI" digunakan sehubungan dengan jenis polimorfik. Namun, di sini kita akan menggunakan istilah ini dalam arti yang lebih luas. Sebagai contoh, kami akan mempertimbangkan fakta bahwa setiap jenis memiliki informasi tentang jenis saat runtime (walaupun Anda hanya bisa mendapatkannya secara statis pada waktu kompilasi, tidak seperti jenis polimorfik). Oleh karena itu, dimungkinkan (dan perlu) untuk membandingkan tipe tipe non-polimorfik (maaf untuk tautologi) saat runtime.
RTTI dapat diakses menggunakan kelas std :: type_info . Kelas ini terletak di file header <typeinfo> . Referensi ke objek kelas ini dapat diperoleh (setidaknya untuk saat ini) hanya menggunakan operator typeid () .

Pola


Fitur lain yang sangat penting dari bahasa yang perlu kita sadari adalah templat. Alat ini cukup kuat dan sangat sulit, bahkan memungkinkan Anda untuk menghasilkan kode pada waktu kompilasi.

Template adalah topik yang sangat luas, dan tidak mungkin untuk mengungkapkannya dalam kerangka artikel, dan itu tidak perlu. Kami berasumsi bahwa pembaca mengerti tentang apa itu. Beberapa poin tidak jelas akan terungkap dalam proses.

Pembungkus argumen diikuti dengan panggilan


Jadi, kami memiliki fungsi tertentu yang mengambil beberapa parameter sebagai input.

Saya akan menunjukkan kepada Anda sketsa kode yang akan menjelaskan niat saya.

 #include <Variadic_args_binder.hpp> #include <string> #include <iostream> #include <vector> #include <any> int f(int a, std::string s) { std::cout << "int: " << a << "\nstring: " << s << std::endl; return 1; } void demo() { std::vector<std::any> params; params.push_back(5); params.push_back(std::string{ "Hello, Delegates!" }); delegates::Variadic_args_binder<int(*)(int, std::string), int, std::string> binder{ f, params }; binder(); } 

Anda mungkin bertanya, bagaimana ini mungkin? Nama kelas Variadic_args_binder memberi tahu Anda bahwa objek mengikat fungsi dan argumen yang perlu Anda sampaikan ketika Anda menyebutnya. Dengan demikian, tetap hanya untuk memanggil binder ini sebagai fungsi tanpa parameter!
Jadi terlihat di luar.

Jika segera, tanpa berpikir, membuat asumsi bagaimana ini dapat diimplementasikan, maka mungkin terlintas dalam pikiran untuk menulis beberapa spesialisasi Variadic_args_binder untuk sejumlah parameter yang berbeda. Namun, ini tidak mungkin jika perlu untuk mendukung jumlah parameter yang tidak terbatas. Dan inilah masalahnya: argumen, sayangnya, perlu diganti menjadi fungsi panggilan statis, yaitu, pada akhirnya untuk kompiler, kode panggilan harus dikurangi menjadi ini:

 fun_ptr(param1, param2, …, paramN); 

Ini adalah cara kerja C ++. Dan semua ini sangat menyulitkan.

Hanya templat ajaib yang bisa menanganinya!

Gagasan utamanya adalah membuat tipe rekursif yang disimpan di setiap level bersarang dari argumen atau fungsi.

Jadi, deklarasikan kelas _Tagged_args_binder :

 namespace delegates::impl { template <typename Func_type, typename... T> class _Tagged_args_binder; } 

Untuk dengan mudah "mentransfer" paket tipe, kami akan membuat tipe tambahan, Type_pack_tag (mengapa ini diperlukan, segera akan menjadi jelas):

 template <typename... T> struct Type_pack_tag { }; 

Sekarang kita membuat spesialisasi kelas _Tagged_args_binder .

Spesialisasi Awal


Seperti yang Anda ketahui, agar rekursi tidak terbatas, perlu untuk mendefinisikan kasus batas.
Spesialisasi berikut adalah awal. Untuk kesederhanaan, saya akan mengutip spesialisasi hanya untuk tipe non-referensi dan tipe referensi nilai.
Spesialisasi untuk nilai parameter langsung:

 template <typename Func_type, typename T1, typename... Types_to_construct> class _Tagged_args_binder<Func_type, Type_pack_tag<T1, Types_to_construct...>, Type_pack_tag<>> { public: static_assert(!std::is_same_v<void, T1>, "Void argument is not allowed"); using Ret_type = std::invoke_result_t<Func_type, T1, Types_to_construct...>; _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(0))) }, ap_caller_part{ func, args } { } auto operator()() { if constexpr(std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::move(ap_arg))); } } auto operator()() const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::move(ap_arg))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<T1>> ap_caller_part; T1 ap_arg; }; 

Argumen pertama untuk panggilan ap_arg dan sisa objek ap_caller_part rekursif disimpan di sini . Perhatikan bahwa tipe T1 "dipindahkan" dari paket jenis pertama di objek ini ke yang kedua di "ekor" objek rekursif.

Spesialisasi untuk tautan nilai:

 template <typename Func_type, typename T1, typename... Types_to_construct> class _Tagged_args_binder<Func_type, Type_pack_tag<T1&&, Types_to_construct...>, Type_pack_tag<>> { using move_ref_T1 = std::add_rvalue_reference_t<std::remove_reference_t<T1>>; public: using Ret_type = std::invoke_result_t<Func_type, move_ref_T1, Types_to_construct>; _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(0))) }, ap_caller_part{ func, args } { } auto operator()() { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg))); } else { return std::forward<Ret_type>(ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg)))); } } auto operator()() const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg))); } else { return std::forward<Ret_type>(ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg)))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<move_ref_T1>> ap_caller_part; std::any ap_arg; }; 


Tautan tautan "tangan kanan" sebenarnya bukan arti tangan kanan. Inilah yang disebut "tautan universal", yang, tergantung pada jenis T1 , menjadi T1 & , atau T1 && . Oleh karena itu, Anda harus menggunakan solusi: pertama, karena spesialisasi ditentukan untuk kedua jenis tautan (itu tidak dikatakan dengan benar, karena alasan yang telah disebutkan) dan untuk parameter non-referensi, ketika Anda membuat contoh template, spesialisasi yang diperlukan akan dipilih, bahkan jika itu adalah tautan tangan kanan; kedua, untuk mentransfer tipe T1 dari paket ke paket, digunakan versi koreksi dari move_ref_T1 , yang diubah menjadi tautan nilai nyata.

Spesialisasi dengan tautan normal dilakukan dengan cara yang sama, dengan koreksi yang diperlukan.

Spesialisasi utama


 template <typename Func_type, typename... Param_type> class _Tagged_args_binder<Func_type, Type_pack_tag<>, Type_pack_tag<Param_type...>> { public: using Ret_type = std::invoke_result_t<Func_type, Param_type...>; inline _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_func{ func } { } inline auto operator()(Param_type... param) { if constexpr(std::is_same_v<void, decltype(ap_func(std::forward<Param_type>(param)...))>) { ap_func(std::forward<Param_type>(param)...); return; } else { return std::forward<Ret_type>(ap_func(std::forward<Param_type>(param)...)); } } inline auto operator()(Param_type... param) const { if constexpr(std::is_same_v<void, Ret_type>) { ap_func(param...); return; } else { return std::forward<Ret_type>(ap_func(param...)); } } private: Func_type ap_func; }; 

Spesialisasi ini bertanggung jawab untuk menyimpan objek fungsional dan, pada kenyataannya, adalah pembungkus di atasnya. Ini adalah tipe rekursif akhir.

Perhatikan bagaimana Type_pack_tag digunakan di sini. Semua tipe parameter sekarang dikompilasi dalam paket kiri. Ini berarti bahwa mereka semua diproses dan dikemas.

Sekarang, saya pikir, menjadi jelas mengapa perlu menggunakan Type_pack_tag . Faktanya adalah, bahasa tidak akan mengizinkan penggunaan dua jenis paket secara berdampingan, misalnya, seperti ini:

 template <typename Func_type, typename T1, typename... Types_to_construct, typename... Param_type> class _Tagged_args_binder<Func_type, T1, Types_to_construct..., Param_type...> { }; 

karena itu, Anda harus memisahkannya menjadi dua paket terpisah dalam dua jenis. Selain itu, Anda perlu memisahkan jenis yang diproses dari yang belum diproses.

Spesialisasi Menengah


Dari spesialisasi menengah, saya akhirnya akan memberikan spesialisasi, sekali lagi, untuk tipe nilai, sisanya adalah dengan analogi:

 template <typename Func_type, typename T1, typename... Types_to_construct, typename... Param_type> class _Tagged_args_binder<Func_type, Type_pack_tag<T1, Types_to_construct...>, Type_pack_tag<Param_type...>> { public: using Ret_type = std::invoke_result_t<Func_type, Param_type..., T1, Types_to_construct...>; static_assert(!std::is_same_v<void, T1>, "Void argument is not allowed"); inline _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(sizeof...(Param_type)))) }, ap_caller_part{ func, args } { } inline auto operator()(Param_type... param) { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg))); } } inline auto operator()(Param_type... param) const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg)); } else { return std::forward<Ret_type>(ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<Param_type..., T1>> ap_caller_part; T1 ap_arg; }; 

Spesialisasi ini dimaksudkan untuk mengemas argumen apa pun kecuali yang pertama.

Kelas pengikat


Kelas _Tagged_args_binder tidak dimaksudkan untuk penggunaan langsung, yang ingin saya tekankan dengan satu garis bawah pada awal namanya. Oleh karena itu, saya akan memberikan kode kelas kecil, yang merupakan semacam "antarmuka" untuk tipe yang jelek dan tidak nyaman ini (yang, bagaimanapun, menggunakan trik C ++ yang agak tidak biasa, yang memberinya pesona, menurut saya):

 namespace cutecpplib::delegates { template <typename Functor_type, typename... Param_type> class Variadic_args_binder { using binder_type = impl::_Tagged_args_binder<Functor_type, Type_pack_tag<Param_type...>, Type_pack_tag<>>; public: using Ret_type = std::invoke_result_t<binder_type>; inline Variadic_args_binder(Functor_type function, Param_type... param) : ap_tagged_binder{ function, param... } { } inline Variadic_args_binder(Functor_type function, std::vector<std::any>& args) : ap_tagged_binder{ function, args } { } inline auto operator()() { return ap_tagged_binder(); } inline auto operator()() const { return ap_tagged_binder(); } private: binder_type ap_tagged_binder; }; } 

Konvensi unihold - meneruskan tautan di dalam std :: any


Pembaca yang penuh perhatian harus memperhatikan bahwa kode tersebut menggunakan fungsi unihold :: reference_any_cast () . Fungsi ini, serta analognya unihold :: pointer_any_cast () , dirancang untuk mengimplementasikan perjanjian pustaka: argumen yang harus dilewati oleh referensi diteruskan oleh pointer ke std :: any .

Fungsi reference_any_cast selalu mengembalikan referensi ke objek, apakah objek itu sendiri disimpan dalam wadah atau hanya pointer ke sana. Jika std :: any berisi objek, maka referensi ke objek ini dikembalikan di dalam wadah; jika itu berisi pointer, maka referensi dikembalikan ke objek yang ditunjuk oleh pointer.

Untuk setiap fungsi, ada opsi untuk versi std :: any dan overload yang konstan untuk menentukan apakah container std :: any memiliki objek atau hanya berisi pointer.

Fungsi harus secara khusus terspesialisasi dalam jenis objek yang disimpan, seperti konversi tipe C ++ dan fungsi templat serupa.

Kode untuk fungsi-fungsi ini:

 template <typename T> std::remove_reference_t<T>& unihold::reference_any_cast(std::any& wrapper) { bool result; return reference_any_cast<T>(wrapper, result); } template <typename T> const std::remove_reference_t<T>& unihold::reference_any_cast(const std::any& wrapper) { bool result; return reference_any_cast<T>(wrapper, result); } template <typename T> std::remove_reference_t<T>& unihold::reference_any_cast(std::any& wrapper, bool& is_owner) { auto ptr = pointer_any_cast<T>(&wrapper, is_owner); if (!ptr) throw std::bad_any_cast{ }; return *ptr; } template <typename T> const std::remove_reference_t<T>& unihold::reference_any_cast(const std::any& wrapper, bool& is_owner) { auto ptr = pointer_any_cast<T>(&wrapper, is_owner); if (!ptr) throw std::bad_any_cast{ }; return *ptr; } template <typename T> std::remove_reference_t<T>* unihold::pointer_any_cast(std::any* wrapper, bool& is_owner) { using namespace std; using NR_T = remove_reference_t<T>; // No_reference_T //     wrapper NR_T** double_ptr_to_original = any_cast<NR_T*>(wrapper); //      wrapper NR_T* ptr_to_copy; if (double_ptr_to_original) { // Wrapper      is_owner = false; return *double_ptr_to_original; } else if (ptr_to_copy = any_cast<NR_T>(wrapper)) { // Wrapper    is_owner = true; return ptr_to_copy; } else { throw bad_any_cast{}; } } template <typename T> const std::remove_reference_t<T>* unihold::pointer_any_cast(const std::any* wrapper, bool& is_owner) { using namespace std; using NR_T = remove_reference_t<T>; // No_reference_T //     wrapper NR_T*const * double_ptr_to_original = any_cast<NR_T*>(wrapper); //      wrapper const NR_T* ptr_to_copy; //remove_reference_t<T>* ptr2 = any_cast<remove_reference_t<T>>(&wrapper); if (double_ptr_to_original) { // Wrapper      is_owner = false; return *double_ptr_to_original; } else if (ptr_to_copy = any_cast<NR_T>(wrapper)) { // Wrapper    is_owner = true; return ptr_to_copy; } else { throw bad_any_cast{}; } } template <typename T> std::remove_reference_t<T>* unihold::pointer_any_cast(std::any* wrapper) { bool result; return pointer_any_cast<T>(wrapper, result); } template <typename T> const std::remove_reference_t<T>* unihold::pointer_any_cast(const std::any* wrapper) { bool result; return pointer_any_cast<T>(wrapper, result); } 

Kesimpulan


Saya mencoba menjelaskan secara singkat salah satu pendekatan yang mungkin untuk memecahkan masalah panggilan fungsi dinamis di C ++. Selanjutnya, ini akan membentuk dasar pustaka delegasi C ++ (pada kenyataannya, saya sudah menulis fungsionalitas utama pustaka, yaitu, delegasi polimorfik, tetapi pustaka masih perlu ditulis ulang sebagaimana mestinya, agar dapat mendemonstrasikan kode, dan menambahkan beberapa fungsi yang belum direalisasi). Dalam waktu dekat saya berencana untuk menyelesaikan pekerjaan di perpustakaan dan memberi tahu bagaimana tepatnya saya mengimplementasikan sisa fungsi delegasi dalam C ++.

PS Menggunakan RTTI akan diperlihatkan di bagian selanjutnya.

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


All Articles