Tentang standar C ++ yang baru

Hari ini saya punya posting yang cukup singkat. Saya mungkin tidak akan menulisnya, tetapi pada Habré dalam komentar Anda sering menemukan pendapat bahwa pro semakin buruk, panitia membuatnya tidak jelas apa yang tidak jelas mengapa, dan umumnya mengembalikan saya pada 2007. Dan kemudian contoh yang jelas tiba-tiba muncul.


Hampir tepat lima tahun yang lalu, saya menulis tentang cara membuat kari di C ++. Nah, jika Anda bisa menulis foo(bar, baz, quux) , maka Anda bisa menulis Curry(foo)(bar)(baz)(quux) . Kemudian C ++ 14 baru saja keluar dan hampir tidak didukung oleh kompiler, sehingga kode hanya menggunakan chip C ++ 11 (ditambah beberapa kruk untuk mensimulasikan fungsi perpustakaan dari C ++ 14).


Dan kemudian saya menemukan kode ini lagi, dan mata saya sakit betapa tepatnya itu. Plus, saya memutar kalender belum lama ini dan memperhatikan bahwa sekarang adalah tahun 2019, dan Anda dapat melihat bagaimana C ++ 17 dapat membuat hidup kita lebih mudah.


Bisakah kita melihat?


Ok, mari kita lihat.


Implementasi asli, dari mana kita akan menari, terlihat seperti ini:


 template<typename F, typename... PrevArgs> class CurryImpl { const F m_f; const std::tuple<PrevArgs...> m_prevArgs; public: CurryImpl (F f, const std::tuple<PrevArgs...>& prev) : m_f { f } , m_prevArgs { prev } { } private: template<typename T> std::result_of_t<F (PrevArgs..., T)> invoke (const T& arg, int) const { return invokeIndexed (arg, std::index_sequence_for<PrevArgs...> {}); } template<typename IF> struct Invoke { template<typename... IArgs> auto operator() (IF fr, IArgs... args) -> decltype (fr (args...)) { return fr (args...); } }; template<typename R, typename C, typename... Args> struct Invoke<R (C::*) (Args...)> { R operator() (R (C::*ptr) (Args...), C c, Args... rest) { return (c.*ptr) (rest...); } R operator() (R (C::*ptr) (Args...), C *c, Args... rest) { return (c->*ptr) (rest...); } }; template<typename T, std::size_t... Is> auto invokeIndexed (const T& arg, std::index_sequence<Is...>) const -> decltype (Invoke<F> {} (m_f, std::get<Is> (m_prevArgs)..., arg)) { return Invoke<F> {} (m_f, std::get<Is> (m_prevArgs)..., arg); } template<typename T> auto invoke (const T& arg, ...) const -> CurryImpl<F, PrevArgs..., T> { return { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } public: template<typename T> auto operator() (const T& arg) const -> decltype (invoke (arg, 0)) { return invoke (arg, 0); } }; template<typename F> CurryImpl<F> Curry (F f) { return { f, {} }; } 

Di m_f terletak functor tersimpan, di m_prevArgs - argumen yang disimpan pada panggilan sebelumnya.


Di sini operator() harus menentukan apakah sudah mungkin untuk memanggil functor yang disimpan, atau apakah perlu untuk terus mengumpulkan argumen, sehingga membuat SFINAE yang cukup standar menggunakan bantuan invoke. Selain itu, untuk memanggil functor (atau memeriksa kemampuannya), kita membahas semua ini dengan layer SFINAE lain untuk memahami bagaimana melakukannya (karena kita perlu memanggil pointer ke anggota dan, katakanlah, fungsi bebas dengan cara yang berbeda), dan untuk ini kami menggunakan struktur pembantu Invoke , yang mungkin tidak lengkap ... Singkatnya, banyak hal.


Nah, hal ini benar-benar menjijikkan bekerja dengan semantik bergerak, penerusan sempurna dan kata-kata lain manis ke jantung tanda plus waktu kita. Memperbaiki ini akan sedikit lebih sulit daripada yang diperlukan, karena selain tugas yang langsung diselesaikan, ada juga banyak kode yang tidak cukup terkait dengannya.


Nah, sekali lagi, di C ++ 11 tidak ada hal-hal seperti std::index_sequence dan hal-hal terkait, atau alias std::result_of_t , jadi kode C ++ 11 murni akan lebih sulit.


Jadi, akhirnya, mari kita beralih ke C ++ 17.


Pertama, kita tidak perlu menentukan operator() tipe kembali operator() , kita dapat menulis hanya:


 template<typename T> auto operator() (const T& arg) const { return invoke (arg, 0); } 

Secara teknis, ini tidak persis sama ("menghubungkan" ditampilkan dengan cara yang berbeda), tetapi dalam kerangka tugas kami ini tidak penting.


Selain itu, kita tidak perlu melakukan SFINAE dengan tangan kita untuk memeriksa kemampuan m_f dengan argumen yang tersimpan. C ++ 17 memberi kita dua fitur keren: constexpr if dan std::is_invocable . Buang segala yang kami miliki sebelumnya dan tulis kerangka operator() baru operator() :


 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) //   else //       arg } 

Cabang kedua sepele, Anda dapat menyalin kode yang sudah:


 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) //   else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } 

Cabang pertama akan lebih menarik. Kita perlu memanggil m_f , meneruskan semua argumen yang tersimpan di m_prevArgs , ditambah arg . Untungnya, kita tidak lagi memerlukan integer_sequence : di C ++ 17 ada fungsi library standar std::apply untuk memanggil fungsi dengan argumen yang disimpan dalam tuple . Hanya kita perlu meletakkan argumen lain ( arg ) di akhir dummy, sehingga kita dapat membuat std::tuple_cat , atau hanya membongkar std::apply 'kita dapat menggunakan lambda generik dummy yang ada (fitur lain yang muncul setelah C ++ 11, meskipun tidak di tanggal 17!). Dalam pengalaman saya, instantiating dummies lambat (dalam waktu perhitungan, tentu saja), jadi saya akan memilih opsi kedua. Di lambda itu sendiri, saya perlu memanggil m_f , dan untuk melakukan ini dengan benar, saya dapat menggunakan fungsi perpustakaan yang muncul di C ++ 17, std::invoke , dengan membuang bantuan Invoke ditulis dengan tangan:


 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) { auto wrapper = [this, &arg] (auto&&... args) { return std::invoke (m_f, std::forward<decltype (args)> (args)..., arg); }; return std::apply (std::move (wrapper), m_prevArgs); } else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } 

Sangat berguna untuk mencatat bagaimana tipe pengembalian yang disimpulkan secara auto memungkinkan Anda untuk mengembalikan nilai dari jenis yang berbeda di cabang yang berbeda if constexpr .


Bagaimanapun, itu pada dasarnya semua. Atau bersama dengan harness yang diperlukan:


 template<typename F, typename... PrevArgs> class CurryImpl { const F m_f; const std::tuple<PrevArgs...> m_prevArgs; public: CurryImpl (F f, const std::tuple<PrevArgs...>& prev) : m_f { f } , m_prevArgs { prev } { } template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) { auto wrapper = [this, &arg] (auto&&... args) { return std::invoke (m_f, std::forward<decltype (args)> (args)..., arg); }; return std::apply (std::move (wrapper), m_prevArgs); } else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } }; template<typename F, typename... Args> CurryImpl<F, Args...> Curry (F f, Args&&... args) { return { f, std::forward_as_tuple (std::forward<Args> (args)...) }; } 

Saya pikir ini adalah peningkatan yang signifikan dibandingkan versi aslinya. Dan lebih mudah dibaca. Bahkan entah bagaimana membosankan, tidak ada tantangan .


Selain itu, kita juga bisa menyingkirkan fungsi Curry dan menggunakan CurryImpl secara langsung, dengan mengandalkan panduan deduksi, tetapi ini paling baik dilakukan ketika kita berurusan dengan penerusan yang sempurna dan sejenisnya. Yang dengan lancar membawa kita ...


Sekarang cukup jelas betapa mengerikannya ini dalam hal menyalin argumen, penerusan sempurna yang tidak menguntungkan ini, dan sejenisnya. Tetapi yang lebih penting, memperbaikinya sekarang jauh lebih mudah. Namun kami, bagaimanapun, akan melakukan ini di postingan selanjutnya.


Alih-alih sebuah kesimpulan


Pertama, di C ++ 20, std::bind_front akan muncul, yang akan mencakup bagian terbesar dari kasus pengguna saya di mana saya membutuhkan hal seperti itu. Anda biasanya dapat membuangnya. Sedih


Kedua, menulis di pro semakin mudah, bahkan jika Anda menulis beberapa jenis kode template dengan metaprogramming. Anda tidak lagi harus memikirkan opsi SFINAE mana yang harus dipilih, cara membongkar boneka, cara memanggil fungsi. Ambil dan tulis saja if constexpr , std::apply , std::invoke . Di satu sisi, itu bagus, saya tidak ingin kembali ke C ++ 14 atau, terutama, 11. Di sisi lain, rasanya seperti lapisan keterampilan singa menjadi tidak perlu. Tidak, masih berguna untuk dapat mengacaukan sesuatu seperti itu pada templat dan memahami bagaimana semua keajaiban perpustakaan ini bekerja di dalam diri Anda, tetapi jika Anda membutuhkannya setiap saat, sekarang ini jauh lebih umum. Ini menyebabkan beberapa emosi aneh.

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


All Articles