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() {
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:
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; } } }
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:
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 ) };
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!