Tutup kontak ADL


Bagaimana cara menuliskan nama Anda dalam sejarah selamanya? Yang pertama terbang ke bulan? Yang pertama bertemu orang asing? Kami memiliki cara yang lebih sederhana - Anda dapat menyesuaikan diri dengan standar bahasa C ++.


Eric Nibler, penulis C ++ Ranges, memberikan contoh yang baik. "Ingat ini. 19 Februari 2019 adalah hari istilah "nibloid" pertama kali diucapkan pada pertemuan WG21, " tulisnya di Twitter.


Memang, jika Anda pergi ke CppReference, di bagian cpp / algoritme / rentangcpp / algoritme / rentang , Anda akan menemukan banyak referensi di sana (niebloid). Untuk ini, templat wiki dsc_niebloid terpisah bahkan telah dibuat.


Sayangnya, saya tidak menemukan artikel resmi lengkap mengenai hal ini dan memutuskan untuk menulis sendiri. Ini adalah perjalanan kecil tapi menarik ke dalam jurang astronot arsitektur, di mana kita bisa terjun ke dalam jurang kegilaan ADL dan berkenalan dengan nibloid.


Penting: Saya bukan tukang las sungguhan, tetapi seorang javist yang terkadang memperbaiki kesalahan dalam kode C ++ sebagaimana diperlukan. Jika Anda mengambil sedikit waktu untuk membantu menemukan kesalahan dalam bernalar, itu akan baik. "Bantu Dasha si pelancong mengumpulkan sesuatu yang masuk akal."


Cari


Pertama, Anda perlu memutuskan persyaratan. Ini adalah hal-hal yang terkenal, tetapi "yang eksplisit lebih baik daripada yang implisit," jadi kami akan membahasnya secara terpisah. Saya tidak menggunakan terminologi nyata berbahasa Rusia, tetapi sebaliknya menggunakan bahasa Inggris. Ini perlu karena bahkan kata "pembatasan" dalam konteks artikel ini dapat dikaitkan dengan setidaknya tiga versi bahasa Inggris, perbedaan antara yang penting untuk dipahami.


Misalnya, dalam C ++ ada konsep pencarian nama atau, dengan kata lain, pencarian: ketika sebuah nama ditemukan dalam sebuah program, ia mengkompilasi dengan deklarasi selama kompilasi.


Pencarian dapat dikualifikasikan (jika namanya ada di sebelah kanan operator izin lingkup :: :), dan tidak terampil dalam kasus lain. Jika pencarian memenuhi syarat, maka kami mengabaikan anggota kelas, namespace atau enumerasi yang sesuai. Orang bisa menyebut ini versi "lengkap" dari catatan (seperti yang tampaknya dilakukan dalam terjemahan Straustrup), tetapi lebih baik meninggalkan ejaan aslinya, karena ini merujuk pada jenis kelengkapan yang sangat spesifik.


ADL


Jika pencarian tidak memenuhi syarat, maka kita perlu memahami persis di mana mencari nama. Dan di sini fitur khusus yang disebut ADL disertakan: pencarian yang bergantung pada argumen , atau yang lain - pencarian untuk Koenig (orang yang menciptakan istilah "anti-pola", yang sedikit simbolis dalam terang dari teks berikut). Nicolai Josuttis dalam bukunya "The C ++ Standard Library: A Tutorial and Reference" menggambarkannya sebagai berikut: "Intinya adalah Anda tidak perlu memenuhi syarat namespace dari fungsi jika setidaknya salah satu tipe argumen didefinisikan dalam namespace dari fungsi ini."


Seperti apa bentuknya?


 #include <iostream> int main() { //  . //   , operator<<    ,  ADL , //    std    std::operator<<(std::ostream&, const char*) std::cout << "Test\n"; //    .      -     . operator<<(std::cout, "Test\n"); // same, using function call notation //    : // Error: 'endl' is not declared in this namespace. //      endl(),  ADL  . std::cout << endl; //  . //    ,       ADL. //     std,   endl      std. endl(std::cout); //    : // Error: 'endl' is not declared in this namespace. //  ,  - (endl) -     . (endl)(std::cout); } 

Turun ke neraka dengan ADL


Tampaknya sederhana. Atau tidak? Pertama, tergantung pada jenis argumen, ADL bekerja dalam sembilan cara berbeda , untuk membunuh dengan sapu.


Kedua, sangat praktis, bayangkan kita memiliki semacam fungsi swap. Ternyata std::swap(obj1,obj2); dan using std::swap; swap(obj1, obj2); using std::swap; swap(obj1, obj2); dapat berperilaku sangat berbeda. Jika ADL diaktifkan, maka dari beberapa swap yang berbeda, yang Anda butuhkan sudah dipilih berdasarkan ruang nama argumen! Bergantung pada sudut pandangnya, idiom ini dapat dianggap sebagai contoh positif dan negatif :-)


Jika menurut Anda ini tidak cukup, Anda bisa memasukkan kayu bakar ke dalam oven topi. Ini baru-baru ini ditulis dengan baik oleh Arthur O'Dwyer . Saya harap dia tidak menghukum saya karena menggunakan teladannya.


Bayangkan Anda memiliki program semacam ini:


 #include <stdio.h> namespace A { struct A {}; void call(void (*f)()) { f(); } } void f() { puts("Hello world"); } int main() { call(f); } 

Tentu saja, itu tidak dikompilasi dengan kesalahan:


 error: use of undeclared identifier 'call'; did you mean 'A::call'? call(f); ^~~~ A::call 

Tetapi jika Anda menambahkan kelebihan fungsi yang sama sekali tidak terpakai di f , maka semuanya akan berfungsi!


 #include <stdio.h> namespace A { struct A {}; void call(void (*f)()) { f(); } } void f() { puts("Hello world"); } void f(A::A); // UNUSED int main() { call(f); } 

Di Visual Studio masih akan pecah, tetapi nasibnya seperti itu, tidak bekerja.


Bagaimana ini bisa terjadi? Mari kita mempelajari standar (tanpa terjemahan, karena terjemahan seperti itu akan menjadi kesalahan besar dari kata kunci):


Jika argumen adalah nama atau alamat dari satu set fungsi yang kelebihan beban dan / atau templat fungsi, entitas dan ruang namanya yang terkait adalah gabungan dari yang terkait dengan masing-masing anggota set, yaitu entitas dan ruang nama yang terkait dengan parameternya jenis dan jenis kembali. [...] Selain itu, jika rangkaian fungsi kelebihan beban yang disebutkan di atas diberi nama dengan templat-id, entitas dan ruang namanya yang terkait juga menyertakan argumen tipe-templat dan templat-templat templat.

Sekarang ambil kode seperti ini:


 #include <stdio.h> namespace B { struct B {}; void call(void (*f)()) { f(); } } template<class T> void f() { puts("Hello world"); } int main() { call(f<B::B>); } 

Dalam kedua kasus, argumen diperoleh yang tidak memiliki tipe. f dan f<B::B> adalah nama-nama himpunan fungsi yang kelebihan beban (dari definisi di atas), dan himpunan tersebut tidak memiliki tipe. Untuk memecah kelebihan beban menjadi satu fungsi, Anda perlu memahami jenis penunjuk fungsi apa yang paling cocok untuk overload call terbaik. Jadi, Anda perlu mengumpulkan satu set kandidat untuk call , yang berarti untuk meluncurkan pencarian nama call . Dan untuk ini ADL akan dimulai!


Tetapi biasanya untuk ADL kita harus tahu jenis-jenis argumen! Dan di sini Dentang, ICC, dan MSVC secara keliru pecah sebagai berikut (tetapi GCC tidak):


 [build] ..\..\main.cpp(15,5): error: use of undeclared identifier 'call'; did you mean 'B::call'? [build] call(f<B::B>); [build] ^~~~ [build] B::call [build] ..\..\main.cpp(4,10): note: 'B::call' declared here [build] void call(void (*f)()) { [build] ^ 

Bahkan pencipta kompiler dengan ADL memiliki hubungan yang sedikit tegang.


Nah, apakah ADL masih tampak seperti ide yang bagus? Di satu sisi, kita tidak perlu lagi menulis kode yang begitu sopan:


 std::cout << "Hello, World!" << std::endl; std::operator<<(std::operator<<(std::cout, "Hello, World!"), "\n"); 

Di sisi lain, kami berdagang untuk singkatnya fakta bahwa sekarang ada sistem yang bekerja dengan cara yang sama sekali tidak manusiawi. Kisah tragis dan agung tentang bagaimana kemudahan menulis Halloworld dapat memengaruhi seluruh bahasa dalam skala puluhan tahun.


Rentang dan konsep


Jika Anda membuka deskripsi perpustakaan Nibler Rangers , maka bahkan sebelum penyebutan nibloid, Anda akan menemukan banyak marker lain yang disebut (konsep) . Ini sudah merupakan hal yang cantik, tetapi untuk berjaga-jaga (untuk orang tua dan javist) saya akan mengingatkan Anda apa itu .


Konsep disebut set himpunan kendala yang berlaku untuk argumen templat untuk memilih kelebihan fungsi terbaik dan spesialisasi templat yang paling cocok.


 template <typename T> concept bool HasStringFunc = requires(T a) { { to_string(a) } -> string; }; void print(HasStringFunc a) { cout << to_string(a) << endl; } 

Di sini kami telah memberlakukan batasan bahwa argumen harus memiliki fungsi to_string yang mengembalikan string. Jika kami mencoba memasukkan permainan ke dalam print yang tidak termasuk dalam batasan, maka kode seperti itu tidak akan dikompilasi.


Ini sangat menyederhanakan kode. Misalnya, lihat bagaimana Nibler melakukan pengurutan dalam rentang-v3 , yang bekerja di C ++ 11/14/17. Ada kode yang luar biasa seperti ini:


 #define CONCEPT_PP_CAT_(X, Y) X ## Y #define CONCEPT_PP_CAT(X, Y) CONCEPT_PP_CAT_(X, Y) /// \addtogroup group-concepts /// @{ #define CONCEPT_REQUIRES_(...) \ int CONCEPT_PP_CAT(_concept_requires_, __LINE__) = 42, \ typename std::enable_if< \ (CONCEPT_PP_CAT(_concept_requires_, __LINE__) == 43) || (__VA_ARGS__), \ int \ >::type = 0 \ /**/ 

Sehingga nanti bisa Anda lakukan:


 struct Sortable_ { template<typename Rng, typename C = ordered_less, typename P = ident, typename I = iterator_t<Rng>> auto requires_() -> decltype( concepts::valid_expr( concepts::model_of<concepts::ForwardRange, Rng>(), concepts::is_true(ranges::Sortable<I, C, P>()) )); }; using Sortable = concepts::models<Sortable_, Rng, C, P>; template<typename Rng, typename C = ordered_less, typename P = ident, CONCEPT_REQUIRES_(!Sortable<Rng, C, P>())> void operator()(Rng &&, C && = C{}, P && = P{}) const { ... 

Saya harap Anda sudah ingin melihat semua ini dan hanya menggunakan konsep yang sudah disiapkan dalam kompiler baru.


Poin Kustomisasi


Hal menarik berikutnya yang dapat ditemukan dalam standar adalah customization.point.object . Mereka secara aktif digunakan di perpustakaan Nibler Ranges.


Titik kustomisasi adalah fungsi yang digunakan oleh pustaka standar sehingga dapat kelebihan beban untuk tipe pengguna di namespace pengguna, dan kelebihan ini dapat ditemukan menggunakan ADL.


Poin kustomisasi dirancang dengan mempertimbangkan prinsip arsitektur berikut ( cust adalah nama untuk beberapa titik kustomisasi imajiner):


  • Kode yang memanggil cust ditulis dalam bentuk std::cust(a) memenuhi syarat atau yang tidak memenuhi syarat: using std::cust; cust(a); using std::cust; cust(a); . Kedua entri harus berperilaku identik. Secara khusus, mereka harus menemukan kelebihan pengguna di namespace yang terkait dengan argumen.
  • Kode yang menggunakan cust dalam bentuk std::cust; cust(a); std::cust; cust(a); seharusnya tidak dapat menghindari pembatasan yang diberlakukan pada std::cust .
  • Panggilan titik khusus harus bekerja secara efisien dan optimal pada setiap kompiler yang cukup modern.
  • Keputusan tidak boleh membuat pelanggaran baru terhadap Aturan Definisi Tunggal (ODR) .

Untuk memahami apa itu, Anda dapat melihat N4381 . Pada pandangan pertama, mereka tampak seperti cara untuk menulis versi begin Anda sendiri, swap , data , dan sejenisnya, dan perpustakaan standar mengambilnya menggunakan ADL.


Pertanyaannya adalah, bagaimana hal ini berbeda dari praktik lama, ketika pengguna menulis overload untuk beberapa orang begin untuk tipe dan namespace sendiri? Dan mengapa mereka bahkan menolak?


Bahkan, ini adalah contoh objek fungsional di std . Tujuan mereka adalah untuk pertama-tama menarik cek tipe (dirancang sebagai konsep) pada semua argumen berturut-turut, dan kemudian mengirimkan panggilan ke fungsi yang benar di std atau menyerah untuk dijual di ADL.


Sebenarnya, ini bukan hal yang akan Anda gunakan dalam program non-perpustakaan biasa. Ini adalah fitur dari perpustakaan standar, yang akan memungkinkan Anda untuk menambahkan pemeriksaan konsep pada titik ekstensi di masa depan, yang pada gilirannya akan mengarah pada tampilan kesalahan yang lebih indah dan dapat dimengerti jika Anda mengacaukan sesuatu di templat.


Pendekatan saat ini untuk poin penyesuaian memiliki beberapa masalah. Pertama, sangat mudah untuk menghancurkan segalanya. Bayangkan kode ini:


 template<class T> void f(T& t1, T& t2) { using std::swap; swap(t1, t2); } 

Jika kita secara tidak sengaja membuat panggilan yang memenuhi syarat ke std::swap(t1, t2) maka versi swap kita sendiri tidak akan pernah mulai, tidak peduli apa yang kita masukkan di sana. Tetapi yang lebih penting, tidak ada cara untuk melampirkan konsep secara terpusat untuk implementasi fungsi kustom tersebut. Dalam N4381 mereka menulis:


“Bayangkan suatu hari nanti, std::begin akan mengharuskan argumennya dimodelkan sebagai konsep Range . Menambahkan batasan seperti itu tidak akan berdampak pada kode secara idiomatis menggunakan std::begin :


 using std::begin; begin(a); 

Lagipula, jika panggilan begin dikirim ke versi overload yang dibuat oleh pengguna, maka pembatasan std::begin diabaikan saja. "


Solusi yang dijelaskan dalam propozal memecahkan kedua masalah, untuk ini kami menggunakan pendekatan dari implementasi spd stul std::begin (Anda dapat melihat godbolt ):


 #include <utility> namespace my_std { namespace detail { struct begin_fn { /*   ,         begin(arg)  arg.begin().  -   . */ template <class T> auto operator()(T&& arg) const { return impl(arg, 1L); } template <class T> auto impl(T&& arg, int) const requires requires { begin(std::declval<T>()); } { return begin(arg); } // ADL template <class T> auto impl(T&& arg, long) const requires requires { std::declval<T>().begin(); } { return arg.begin(); } // ... }; } //        inline constexpr detail::begin_fn begin{}; } 

Panggilan berkualitas dari beberapa my_std::begin(someObject) selalu melewati my_std::detail::begin_fn - dan itu bagus. Apa yang terjadi pada panggilan yang tidak memenuhi syarat? Mari kita baca makalah kita lagi:


“Dalam kasus ketika mulai dipanggil tanpa kualifikasi segera setelah penampilan my_std::begin di dalam lingkup, situasinya agak berubah. Pada tahap pertama pencarian, nama begin menyelesaikan ke objek global my_std::begin . Karena pencarian menemukan objek, bukan fungsi, fase kedua pencarian tidak dilakukan. Dengan kata lain, jika my_std::begin adalah sebuah objek, maka menggunakan konstruksi my_std::detail::begin_fn begin; begin(a); my_std::detail::begin_fn begin; begin(a); sederajat dengan std::begin(a); "Dan seperti yang telah kita lihat, ini meluncurkan ADL kustom."


Itulah sebabnya validasi konsep dapat dilakukan dalam objek fungsi di std sebelum ADL memanggil fungsi yang disediakan oleh pengguna. Tidak ada cara untuk mengelabui perilaku ini.


Bagaimana titik kustomisasi menyesuaikan?


Bahkan, "objek titik kustomisasi" (CPO) bukan nama yang baik. Dari namanya tidak jelas bagaimana mereka berkembang, mekanisme apa yang ada di bawah tenda, fungsi apa yang mereka sukai ...


Yang mengarahkan kita pada istilah "nibloid." Nibloid adalah CPO yang memanggil fungsi X jika didefinisikan di kelas, jika tidak maka akan memanggil fungsi X jika ada fungsi bebas yang sesuai, jika tidak ia mencoba untuk menjalankan beberapa fallback dari fungsi X.


Jadi misalnya, ranges::swap nibloid ranges::swap ketika memanggil ranges::swap(a, b) pertama-tama akan mencoba memanggil a.swap(b) . Jika tidak ada metode seperti itu, ia akan mencoba memanggil swap(a, b) menggunakan ADL. Jika ini tidak berhasil, coba auto tmp = std::move(a); a = std::move(b); b = std::move(tmp) auto tmp = std::move(a); a = std::move(b); b = std::move(tmp) auto tmp = std::move(a); a = std::move(b); b = std::move(tmp) .


Ringkasan


Seperti yang dibicarakan Matt di Twitter, Dave pernah menyarankan agar objek fungsional "berfungsi" dengan ADL seperti fungsi biasa, karena alasan konsistensi. Ironinya adalah kemampuan mereka untuk menonaktifkan ADL dan tidak terlihat olehnya kini telah menjadi keuntungan utama mereka.


Seluruh artikel ini adalah persiapan untuk ini.


" Aku hanya mengerti segalanya, itu saja. Maukah kamu mendengarkan ?


Pernahkah Anda melihat sesuatu, dan itu tampak gila, dan kemudian dalam cahaya yang berbeda menyala
hal-hal gila melihat mereka normal?



Jangan takut. Jangan takut. Saya merasa sangat baik hati. Semuanya akan baik-baik saja. Saya merasa tidak enak selama bertahun-tahun. Semuanya akan baik-baik saja.



Menit periklanan. Sudah minggu ini , 19-20 April, C ++ Russia 2019 akan diadakan - sebuah konferensi yang diisi dengan presentasi hardcore baik pada bahasanya sendiri maupun pada isu-isu praktis seperti multithreading dan kinerja. Omong-omong, konferensi dibuka oleh Nicolai Josuttis, penulis The C ++ Standard Library: A Tutorial and Reference , disebutkan dalam artikel. Anda dapat membiasakan diri dengan program ini dan membeli tiket di situs web resmi . Hanya ada sedikit waktu tersisa, ini adalah kesempatan terakhir.

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


All Articles