Cara membuat SFINAE ramping dan dapat diandalkan

Halo lagi. Kami membagikan kepada Anda sebuah artikel yang menarik, terjemahan yang disiapkan khusus untuk siswa kursus "Pengembang C ++" .





Hari ini kami memiliki posting tamu oleh dám Balázs. Adam adalah seorang insinyur perangkat lunak di Verizon Smart Communities Hungaria dan mengembangkan analitik video untuk sistem embedded. Salah satu gairah hidupnya adalah optimalisasi waktu kompilasi, jadi dia langsung setuju untuk menulis posting tamu tentang topik ini. Anda dapat menemukan Adam online di LinkedIn .

Dalam serangkaian artikel tentang cara membuat SFINAE anggun , kami melihat cara membuat templat SFINAE kami cukup ringkas dan ekspresif .

Lihatlah bentuk aslinya:

template<typename T> class MyClass { public: void MyClass(T const& x){} template<typename T_ = T> void f(T&& x, typename std::enable_if<!std::is_reference<T_>::value, std::nullptr_t>::type = nullptr){} }; 


Dan bandingkan dengan bentuk yang lebih ekspresif ini:

 template<typename T> using IsNotReference = std::enable_if_t<!std::is_reference_v<T>>; template<typename T> class MyClass { public: void f(T const& x){} template<typename T_ = T, typename = IsNotReference <T_>> void f(T&& x){} }; 

Kita cukup percaya bahwa sudah dimungkinkan untuk bersantai dan mulai menggunakannya dalam produksi. Kita bisa, ini berfungsi dalam banyak kasus, tetapi - ketika kita berbicara tentang antarmuka - kode kita harus aman dan dapat diandalkan. Benarkah begitu? Mari kita coba meretasnya!

Cacat # 1: SFINAE Dapat Di-Bypass


Biasanya, SFINAE digunakan untuk menonaktifkan bagian dari kode tergantung pada kondisinya. Ini bisa sangat berguna jika kita perlu mengimplementasikan, misalnya, fungsi abs yang ditentukan pengguna untuk alasan apa pun (kelas aritmatika yang ditentukan pengguna, optimisasi untuk peralatan tertentu, untuk tujuan pelatihan, dll.):

 template< typename T > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } int main() { int a{ std::numeric_limits< int >::max() }; std::cout << "a: " << a << " myAbs( a ): " << myAbs( a ) << std::endl; } 

Program ini menampilkan yang berikut, yang terlihat cukup normal:

 a: 2147483647 myAbs( a ): 2147483647 

Tetapi kita dapat memanggil fungsi abs kita dengan argumen unsigned T , dan efeknya akan menjadi bencana:

 nt main() { unsigned int a{ std::numeric_limits< unsigned int >::max() }; std::cout << "a: " << a << " myAbs( a ): " << myAbs( a ) << std::endl; } 

Memang, sekarang program menampilkan:

a: 4294967295 myAbs( a ): 1

Fungsi kami tidak dirancang untuk berfungsi dengan argumen yang tidak ditandatangani, jadi kami harus membatasi set T dengan SFINAE:

 template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T > >; template< typename T, typename = IsSigned< T > > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } 

Kode berfungsi seperti yang diharapkan: memanggil myAbs dengan tipe yang tidak ditandatangani menyebabkan kesalahan waktu kompilasi:

candidate template ignored: requirement 'std::is_signed_v< unsigned int>' was not satisfied [with T = unsigned int]

Meretas Status SFINAE


Lalu apa yang salah dengan fungsi ini? Untuk menjawab pertanyaan ini, kita perlu memeriksa bagaimana myAbs mengimplementasikan SFINAE.

 template< typename T, typename = IsSigned<T> > T myAbs( T val ); 

myAbs adalah templat fungsi dengan dua jenis parameter templat masukan. Yang pertama adalah tipe aktual dari argumen fungsi, yang kedua adalah tipe anonim default IsSigned < T > (jika tidak std::enable_if_t < std::is_signed_v < T > > atau std::enable_if_t < std::is_signed_v < T > > std::enable_if < std::is_signed_v < T>, void>::type , yang void atau gagal substitusi).

Bagaimana kita bisa memanggil myAbs ? Ada 3 cara:

 int a{ myAbs( -5 ) }; int b{ myAbs< int >( -5 ) }; int c{ myAbs< int, void >( -5 ) }; 

Panggilan pertama dan kedua mudah, tetapi yang ketiga menarik: apa argumen dari template void ?

Parameter templat kedua adalah anonim, memiliki tipe default, tetapi masih merupakan parameter templat, sehingga Anda dapat menentukannya secara eksplisit. Apakah ini masalah? Dalam hal ini, ini benar-benar masalah besar. Kita bisa menggunakan formulir ketiga untuk berkeliling memeriksa SFINAE kami:

 unsigned int d{ myAbs< unsigned int, void >( 5u ) }; unsigned int e{ myAbs< unsigned int, void >( std::numeric_limits< unsigned int >::max() ) }; 

Kode ini mengkompilasi dengan baik, tetapi mengarah ke hasil yang merusak, untuk menghindari yang kami gunakan SFINAE:

 a: 4294967295 myAbs( a ): 1 

Kami akan memecahkan masalah ini - tetapi pertama-tama: apakah ada kerugian lain? Baiklah ...

Cacat # 2: Kami Tidak Dapat Memiliki Implementasi Khusus


Penggunaan umum lain dari SFINAE adalah untuk menyediakan implementasi spesifik untuk kondisi waktu kompilasi tertentu. Bagaimana jika kita tidak ingin sepenuhnya melarang panggilan myAbs dengan nilai yang myAbs dan memberikan implementasi yang sepele untuk kasus-kasus ini? Kita dapat menggunakan jika constexpr di C ++ 17 (kita akan membahas ini nanti), atau kita dapat:

  template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T > >; template< typename T > using IsUnsigned = std::enable_if_t< std::is_unsigned_v< T > >; template< typename T, typename = IsSigned< T > > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } template< typename T, typename = IsUnsigned< T > > T myAbs( T val ) { return val; } 

Tapi apa itu?

 error: template parameter redefines default argument template< typename T, typename = IsUnsigned< T > > note: previous default template argument defined here template< typename T, typename = IsSigned< T > > 

Oh, standar C ++ (C ++ 17; §17.1.16) menyatakan sebagai berikut :

"Argumen default tidak boleh diberikan ke parameter template dengan dua deklarasi berbeda dalam cakupan yang sama."

Ups, inilah yang kami lakukan ...

Mengapa tidak menggunakan regular jika?


Kami hanya bisa menggunakan jika saat runtime:

 template< typename T > T myAbs( T val ) { if( std::is_signed_v< T > ) { return ( ( val <= -1 ) ? -val : val ); } else { return val; } } 

Compiler akan mengoptimalkan kondisinya karena if (std::is_signed_v < T>) menjadi if (true) atau if (false) setelah membuat templat. Ya, dengan implementasi myAbs kami saat ini myAbs ini akan berhasil. Tapi secara keseluruhan, ini memberlakukan batasan besar: pernyataan if and else harus valid untuk setiap T Bagaimana jika kami mengubah sedikit implementasi kami:

 template< typename T > T myAbs( T val ) { if( std::is_signed_v< T > ) { return std::abs( val ); } else { return val; } } int main() { unsigned int a{ myAbs( 5u ) }; } 

Kode kami akan segera macet:

 error: call of overloaded 'abs(unsigned int&)' is ambiguous 

Pembatasan ini adalah apa yang dihilangkan oleh SFINAE: kita dapat menulis kode yang hanya valid untuk subset dari T (dalam myAbs itu hanya berlaku untuk tipe yang tidak ditandatangani atau hanya berlaku untuk tipe yang ditandatangani).

Solusi: formulir lain untuk SFINAE


Apa yang bisa kita lakukan untuk mengatasi kekurangan ini? Untuk masalah pertama, kami harus memaksa pemeriksaan SFINAE kami terlepas dari bagaimana pengguna menjalankan fungsi kami. Saat ini, pengujian kami dapat dielakkan ketika kompiler tidak memerlukan jenis default untuk parameter templat kedua.

Bagaimana jika kita menggunakan kode SFINAE untuk mendeklarasikan tipe parameter templat alih-alih memberikan tipe default? Mari kita coba:

 template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >; template< typename T, IsSigned< T > = true > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } int main() { //int a{ myAbs( 5u ) }; int b{ myAbs< int >( 5u ) }; //int c{ myAbs< unsigned int, true >( 5u ) }; } 

Kita perlu IsSigned menjadi tipe selain batal dalam kasus yang valid, karena kami ingin memberikan nilai default untuk tipe ini. Tidak ada nilai untuk tipe void, jadi kita harus menggunakan sesuatu yang lain: bool, int, enum, nullptr_t, dll. Biasanya saya menggunakan bool - dalam hal ini ekspresi terlihat bermakna:

 template< typename T, IsSigned< T > = true > 

Itu berhasil! Untuk myAbs (5u) kompiler melempar kesalahan, seperti sebelumnya:

 candidate template ignored: requirement 'std::is_signed_v<unsigned int>' was not satisfied [with T = unsigned int 

Panggilan kedua, myAbs < int> (5u) masih valid, kami memberi tahu tipe kompiler T secara eksplisit, sehingga mengkonversi 5u menjadi int .

Akhirnya, kita tidak bisa lagi melacak myAbs di jari: myAbs < unsigned int, true> (5u) melempar kesalahan. Tidak masalah jika kami memberikan nilai default dalam panggilan atau tidak, bagian dari ekspresi SFINAE tetap dievaluasi, karena kompiler memerlukan tipe argumen dari nilai template anonim.

Kita dapat beralih ke masalah berikutnya - tetapi tunggu sebentar! Saya pikir kita tidak lagi mengesampingkan argumen default untuk parameter template yang sama. Apa situasi aslinya?

 template< typename T, typename = IsUnsigned< T > > T myAbs( T val ); template< typename T, typename = IsSigned< T > > T myAbs( T val ); 

Tetapi sekarang dengan kode saat ini:

 template< typename T, IsUnsigned< T > = true > T myAbs( T val ); template< typename T, IsSigned< T > = true > T myAbs( T val ); 

Itu terlihat sangat mirip dengan kode sebelumnya, jadi kita mungkin berpikir bahwa ini tidak akan berhasil juga, tetapi sebenarnya kode ini tidak memiliki masalah yang sama. Apa itu IsUnsigned < T> ? Pencarian bool atau gagal. Dan apa yang IsSigned < T> ? Hal yang sama, tetapi jika salah satunya adalah Bool, yang lain adalah pencarian gagal.

Ini berarti bahwa kita tidak mengesampingkan argumen default, karena hanya ada satu fungsi dengan bool argumen templat, yang lainnya adalah substitusi yang gagal, sehingga tidak ada.

Gula sintaksis


UPD Paragraf ini telah dihapus oleh penulis karena kesalahan yang ditemukan di dalamnya.

Versi lama dari C ++


Semua hal di atas bekerja dengan C ++ 11, satu-satunya perbedaan adalah verbositas dari definisi pembatasan antara versi standar:

 //C++11 template< typename T > using IsSigned = typename std::enable_if< std::is_signed< T >::value, bool >::type; //C++14 - std::enable_if_t template< typename T > using IsSigned = std::enable_if_t< std::is_signed< T >::value, bool >; //C++17 - std::is_signed_v template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >; 

Tetapi templatnya tetap sama:

 template< typename T, IsSigned< T > = true > 

Dalam C ++ 98 yang baik, tidak ada alias templat, selain itu, templat fungsi tidak dapat memiliki jenis atau nilai default. Kami dapat memasukkan kode SFINAE kami ke dalam tipe hasil atau hanya ke dalam daftar parameter fungsi. Opsi kedua direkomendasikan karena konstruktor tidak memiliki tipe hasil. Yang terbaik yang bisa kita lakukan adalah sesuatu seperti ini:

 template< typename T > T myAbs( T val, typename my_enable_if< my_is_signed< T >::value, bool >::type = true ) { return( ( val <= -1 ) ? -val : val ); } 

Hanya untuk perbandingan - versi modern C ++:

 template< typename T, IsSigned< T > = true > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } 

Versi C ++ 98 jelek, memperkenalkan parameter yang tidak berarti, tetapi berfungsi - Anda dapat menggunakannya jika benar-benar diperlukan. Dan ya: my_enable_if dan my_is_signed harus diimplementasikan ( std :: enable_if std :: is_signed baru di C ++ 11).

Kondisi saat ini


C ++ 17 diperkenalkan if constexpr , sebuah metode untuk membuang kode berdasarkan kondisi pada waktu kompilasi. Kedua pernyataan if and else harus benar secara sintaksis, tetapi kondisinya akan dievaluasi pada waktu kompilasi.

 template< typename T > T myAbs( T val ) { if constexpr( std::is_signed_v< T > ) { return( ( val <= -1 ) ? -val : val ); } else { if constexpr( std::is_unsigned_v< T > ) { return val; } /*else { static_assert( false, "T must be signed or unsigned arithmetic type." ); }*/ } } 

Seperti yang bisa kita lihat, fungsi perut kita menjadi lebih padat dan mudah dibaca. Namun, penanganan tipe yang tidak sesuai tidak langsung. static_assert tanpa syarat yang static_assert membuat pernyataan ini kurang konsisten, yang dilarang oleh standar, terlepas dari apakah itu dibuang atau tidak.

Untungnya, ada celah: di objek templat, operator yang dijatuhkan tidak dibuat jika kondisinya tidak bergantung pada nilai. Hebat!

Jadi satu-satunya masalah dengan kode kami adalah crash pada definisi template. Jika kita dapat menunda evaluasi static_assert hingga waktu templat dibuat, masalahnya akan terpecahkan: itu akan dibuat jika dan hanya jika semua kondisi kita salah. Tetapi bagaimana kita bisa menunda static_assert sampai templat dibuat? Jadikan kondisinya tergantung pada jenisnya!

 template< typename > inline constexpr bool dependent_false_v{ false }; template< typename T > T myAbs( T val ) { if constexpr( std::is_signed_v< T > ) { return( ( val <= -1 ) ? -val : val ); } else { if constexpr( std::is_unsigned_v< T > ) { return val; } else { static_assert( dependent_false_v< T >, "Unsupported type" ); } } } 

Tentang masa depan


Kami sudah sangat dekat, tetapi kami harus menunggu sebentar sampai C ++ 20 membawa solusi akhir: konsep! Ini sepenuhnya akan mengubah cara templat (dan SFINAE) digunakan.

Singkatnya: konsep dapat digunakan untuk membatasi sekumpulan argumen yang diterima untuk parameter templat. Untuk fungsi perut kami, kami dapat menggunakan konsep berikut:

 template< typename T > concept bool Arithmetic() { return std::is_arithmetic_v< T >; } 

Dan bagaimana kita bisa menggunakan konsep? Ada tiga cara:

 //   template< typename T > requires Arithmetic< T >() T myAbs( T val ); //   template< Arithmetic T > T myAbs( T val ); //  Arithmetic myAbs( Arithmetic val ); 

Perhatikan bahwa formulir ketiga masih mendeklarasikan fungsi templat! Berikut ini adalah implementasi penuh myAbs di C ++ 20:

 template< typename T > concept bool Arithmetic() { return std::is_arithmetic_v< T >; } Arithmetic myAbs( Arithmetic val ) { if constexpr( std::is_signed_v< decltype( val ) > ) { return( ( val <= -1 ) ? -val : val ); } else { return val; } } int main() { unsigned int a{ myAbs( 5u ) }; int b{ myAbs< int >( 5u ) }; //std::string c{ myAbs( "d" ) }; } 

Panggilan yang dikomentari memberikan kesalahan berikut:

 error: cannot call function 'auto myAbs(auto:1) [with auto:1 = const char*]' constraints not satisfied within 'template<class T> concept bool Arithmetic() [with T = const char*]' concept bool Arithmetic(){ ^~~~~~~~~~ 'std::is_arithmetic_v' evaluated to false 

Saya mendorong semua orang untuk berani menggunakan metode ini dalam kode produksi, waktu kompilasi lebih murah daripada runtime. Selamat Bersenang-senang!

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


All Articles