Pengurangan argumen templat kelas


Standar C ++ 17 menambahkan fitur baru ke bahasa: Pengurangan Argumen Templat Kelas (CTAD) . Seiring dengan fitur-fitur baru di C ++, secara tradisional menambahkan cara baru untuk memotret anggota tubuh mereka sendiri. Dalam artikel ini kita akan memahami apa CTAD itu, apa yang digunakan untuk, bagaimana menyederhanakan hidup, dan apa jebakan yang dikandungnya.


Mari kita mulai dari jauh


Ingat apa yang dimaksud dengan Pengurangan Argumen Templat dan untuk apa. Jika Anda merasa cukup percaya diri dengan templat C ++, Anda dapat melewati bagian ini dan segera melanjutkan ke yang berikutnya.


Sebelum ke C ++ 17, output dari templat parameter hanya diterapkan pada templat fungsi. Saat membuat contoh templat fungsi, Anda tidak dapat secara eksplisit menentukan argumen templat yang dapat disimpulkan dari jenis argumen fungsi yang sebenarnya. Aturan untuk menyimpulkan cukup rumit, mereka dikhususkan untuk seluruh bagian 17.9.2 dalam Standar [temp.deduct] (selanjutnya saya merujuk pada versi draft Standar yang tersedia secara bebas; dalam versi mendatang, penomoran bagian dapat berubah, jadi saya sarankan mencari dengan kode mnemonik yang ditentukan dalam kurung kotak).


Kami tidak akan menganalisis secara terperinci semua seluk-beluk aturan ini, hanya diperlukan oleh pengembang kompiler. Untuk penggunaan praktis, cukup mengingat aturan sederhana: kompiler dapat secara independen menurunkan argumen templat fungsi, jika ini dapat dilakukan dengan jelas berdasarkan informasi yang tersedia. Saat menurunkan jenis parameter parameter templat, transformasi standar diterapkan seperti saat memanggil fungsi biasa ( const dibuang dari tipe literal, array dikurangi menjadi pointer, referensi fungsi dikurangi menjadi pointer fungsi, dll.).


template <typename T> void func(T t) { // ... } int some_func(double d) { return static_cast<int>(d); } int main() { const int i = 123; func(i); // func<int> char arr[] = "Some text"; func(arr); // func<char *> func(some_func); // func<int (*)(double)> return 0; } 

Semua ini menyederhanakan penggunaan templat fungsi, tetapi, sayangnya, sepenuhnya tidak dapat diterapkan pada templat kelas. Saat membuat instance templat kelas, semua parameter templat non-default harus ditentukan secara eksplisit. Karena properti yang tidak menyenangkan ini, seluruh keluarga fungsi gratis dengan awalan make_ muncul di perpustakaan standar: make_unique , make_share , make_pair , make_tuple , dll.


 //  auto tup1 = std::tuple<int, char, double>(123, 'a', 40.0); //   auto tup2 = std::make_tuple(123, 'a', 40.0); 

Baru di C ++ 17


Dalam Standar baru, dengan analogi dengan parameter templat fungsi, parameter templat kelas diturunkan dari argumen yang disebut konstruktor:


 std::pair pr(false, 45.67); // std::pair<bool, double> std::tuple tup(123, 'a', 40.0); // std::tuple<int, char, double> std::less l; // std::less<void>,     std::less<> l template <typename T> struct A { A(T,T); }; auto y = new A{1, 2}; //  A<int> auto lck = std::lock_guard(mtx); // std::lock_guard<std::mutex> std::copy_n(vi1, 3, std::back_insert_iterator(vi2)); //       template <typename T> struct F { F(T); } std::for_each(vi.begin(), vi.end(), Foo([&](int i) {...})); // F<lambda> 

Segera perlu disebutkan pembatasan CTAD yang berlaku pada saat C ++ 17 (mungkin pembatasan ini akan dihapus dalam versi Standar yang akan datang):


  • CTAD tidak berfungsi dengan alias template:

 template <typename X> using PairIntX = std::pair<int, X>; PairIntX p{1, true}; //   

  • CTAD tidak mengizinkan sebagian hasil argumen (cara kerjanya untuk Pengurangan Argumen Templat biasa):

 std::pair p{1, 5}; // OK std::pair<double> q{1, 5}; // ,   std::pair<double, int> r{1, 5}; // OK 

Selain itu, kompiler tidak akan dapat menyimpulkan jenis parameter templat yang tidak secara eksplisit terkait dengan jenis argumen konstruktor. Contoh paling sederhana adalah konstruktor kontainer yang menerima sepasang iterator:


 template <typename T> struct MyVector { template <typename It> MyVector(It from, It to); }; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //     T   It 

Jenis ini tidak terkait langsung dengan T , meskipun kami pengembang tahu persis cara mendapatkannya. Untuk memberi tahu kompiler bagaimana cara menghasilkan tipe yang tidak berhubungan secara langsung, konstruksi bahasa baru muncul di C ++ 17 - panduan deduksi , yang akan kita bahas di bagian selanjutnya.


Panduan pengabdian


Untuk contoh di atas, panduan deduksi akan terlihat seperti ini:


 template <typename It> MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; 

Di sini kita memberi tahu kompiler bahwa untuk konstruktor dengan dua parameter dari tipe yang sama, Anda dapat menentukan tipe T menggunakan std::iterator_traits<It>::value_type konstruksi std::iterator_traits<It>::value_type . Harap dicatat bahwa panduan deduksi berada di luar definisi kelas, ini memungkinkan Anda untuk menyesuaikan perilaku kelas eksternal, termasuk kelas dari C ++ Standard Library.


Deskripsi formal sintaks panduan deduksi diberikan dalam C ++ Standard 17 di bagian 17.10 [temp.deduct.guide] :


 [explicit] template-name (parameter-declaration-clause) -> simple-template-id; 

Kata kunci eksplisit sebelum panduan pemotongan melarang penggunaannya dengan inisialisasi daftar salin :


 template <typename It> explicit MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //  MyVector v3 = {dv.begin(), dv.end()}; //   

Omong-omong, panduan deduksi tidak harus menjadi templat:


 template<class T> struct S { S(T); }; S(char const*) -> S<std::string>; S s{"hello"}; // S<std::string> 

Algoritma CTAD terperinci


Aturan formal untuk memperoleh argumen templat kelas dijelaskan secara rinci dalam klausa 16.3.1.8 [over.match.class.deduct] dari C ++ Standard 17. Mari kita coba mencari tahu mereka.


Jadi, kami memiliki tipe template C yang menerapkan CTAD. Untuk memilih konstruktor dan parameter mana yang harus dipanggil, untuk C , banyak fungsi templat dibentuk sesuai dengan aturan berikut:


  • Untuk setiap konstruktor Ci , fungsi template Fi dummy dihasilkan. Parameter template Fi adalah parameter C , diikuti oleh parameter template Ci (jika ada), termasuk parameter dengan nilai default. Tipe parameter dari fungsi Fi sesuai dengan tipe parameter konstruktor Ci . Mengembalikan fungsi dummy Tipe Fi C dengan argumen yang cocok dengan parameter template C.

Kodesemu:


 template <typename T, typename U> class C { public: template <typename V, typename W = A> C(V, W); }; //    template <typename T, typename U, typename V, typename W = A> C<T, U> Fi(V, W); 

  • Jika tipe C tidak didefinisikan, atau tidak ada konstruktor yang ditentukan, aturan di atas berlaku untuk konstruktor hipotetis C () .
  • Fungsi dummy tambahan dihasilkan untuk konstruktor C Β© , untuk itu, mereka bahkan datang dengan nama khusus: copy deduksi kandidat .
  • Untuk setiap panduan deduksi , fungsi dummy Fi juga dihasilkan dengan parameter template dan argumen panduan deduksi dan nilai balik yang sesuai dengan jenis di sebelah kanan -> dalam panduan deduksi (dalam definisi formal disebut simple-template-id ).

Kodesemu:


 template <typename T, typename V> C(T, V) -> C<typename DT<T>, typename DT<V>>; //    template <typename T, typename V> C<typename DT<T>, typename DT<V>> Fi(T,V); 

Selanjutnya, untuk himpunan fungsi boneka Fi , aturan yang biasa untuk mengeluarkan parameter template dan resolusi kelebihan diterapkan dengan satu pengecualian: ketika fungsi boneka dipanggil dengan daftar inisialisasi yang terdiri dari parameter tunggal dari tipe cv U , di mana U adalah spesialisasi C atau jenis yang diwarisi dari spesialisasi C (untuk berjaga-jaga, saya akan mengklarifikasi bahwa cv == const volatile ; catatan seperti itu berarti tipe U , const U , volatile U dan const volatile U diperlakukan dengan cara yang sama), aturan yang memberikan prioritas pada konstruktor C(std::initializer_list<>) (dilewati untuk rincian daftar inisi lisasi dapat ditemukan pada klausa 16.3.1.7 [over.match.list] dari C ++ Standard 17). Contoh:


 std::vector v1{1, 2}; // std::vector<int> std::vector v2{v1}; // std::vector<int>,   std::vector<std::vector<int>> 

Akhirnya, jika mungkin untuk memilih satu-satunya fungsi boneka yang paling cocok, maka konstruktor atau panduan deduksi yang sesuai dipilih. Jika tidak ada yang cocok, atau ada beberapa yang sama-sama cocok, kompiler melaporkan kesalahan.


Perangkap


CTAD digunakan untuk menginisialisasi objek, dan inisialisasi secara tradisional merupakan bagian yang sangat membingungkan dari bahasa C ++. Dengan penambahan inisialisasi seragam dalam C ++ 11, cara untuk melepaskan kaki Anda hanya meningkat. Sekarang Anda dapat memanggil konstruktor untuk objek dengan kurung bulat dan keriting. Dalam banyak kasus, kedua opsi ini bekerja sama, tetapi tidak selalu:


 std::vector v1{8, 15}; // [8, 15] std::vector v2(8, 15); // [15, 15, … 15] (8 ) std::vector v3{8}; // [8] std::vector v4(8); //   

Sejauh ini, semuanya tampaknya cukup logis: v1 dan v3 memanggil konstruktor yang mengambil std::initializer_list<int> , int disimpulkan dari parameter; v4 tidak dapat menemukan konstruktor yang hanya mengambil satu parameter tipe int . Tapi ini masih bunga, beri di depan:


 std::vector v5{"hi", "world"}; // [β€œhi”, β€œworld”] std::vector v6("hi", "world"); // ?? 

v5 , seperti yang diharapkan, akan bertipe std::vector<const char*> dan diinisialisasi dengan dua elemen, tetapi baris berikutnya melakukan sesuatu yang sama sekali berbeda. Untuk vektor, hanya ada satu konstruktor yang mengambil dua parameter dari jenis yang sama:


 template< class InputIt > vector( InputIt first, InputIt last, const Allocator& alloc = Allocator() ); 

berkat panduan deduksi untuk std::vector "hai" dan "world" akan diperlakukan sebagai iterator, dan semua elemen yang terletak "antara" akan ditambahkan ke vektor bertipe std::vector<char> . Jika kita beruntung dan dua konstanta string ini berada dalam memori berturut-turut, maka tiga elemen akan jatuh ke dalam vektor: 'h', 'i', '\ x00', tetapi, kemungkinan besar, kode tersebut akan menyebabkan pelanggaran perlindungan memori dan crash program.


Bahan yang digunakan


Draf Standar C ++ 17
CTAD
CppCon 2018: Stephan T. Lavavej "Pengurangan Argumen Template Kelas untuk Semua Orang"

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


All Articles