Konsep: menyederhanakan implementasi kelas STD Utility


Konsep yang muncul dalam C ++ 20 adalah topik yang panjang dan banyak dibahas. Terlepas dari kelebihan bahan yang terakumulasi selama bertahun-tahun (termasuk pidato para pakar kelas dunia), masih ada kebingungan di antara para programer terapan (yang tidak tertidur setiap hari dengan standar) apa konsep C ++ 20 dan apakah mereka kita perlu jika ada enable_if diperiksa selama bertahun-tahun. Sebagian kesalahannya adalah bagaimana konsep berkembang lebih dari ~ 15 tahun (Konsep Lengkap + Peta Konsep -> Konsep Lite), dan sebagian karena konsep tersebut ternyata tidak seperti alat serupa dalam bahasa lain (batas generik Java / C #, ciri-ciri Rust,. ..)


Di bawah potongan video dan transkrip dari sebuah laporan oleh Andrey Davydov dari tim ReSharper C ++ dari konferensi C ++ Russia 2019 . Andrey membuat tinjauan singkat tentang inovasi terkait konsep C ++ 20, setelah itu ia memeriksa implementasi beberapa kelas dan fungsi STL, membandingkan solusi C ++ 17 dan C ++ 20. Selanjutnya ceritanya adalah atas namanya.



Bicara tentang konsep. Ini adalah topik yang agak rumit dan luas, jadi ketika mempersiapkan laporan, saya mengalami kesulitan. Saya memutuskan untuk beralih ke pengalaman salah satu pembicara terbaik dari komunitas C ++ Andrei Alexandrescu .


Pada bulan November 2018, berbicara pada pembukaan Rapat C ++ , Andrei bertanya kepada hadirin apa yang akan menjadi fitur besar selanjutnya dari C ++:


  • konsep
  • metaclasses
  • atau introspeksi?

Mari kita mulai dengan pertanyaan ini. Apakah Anda pikir fitur besar berikutnya dalam C ++ akan menjadi konsep?


Menurut Alexandrescu, konsep itu membosankan. Ini adalah hal yang membosankan yang saya sarankan Anda lakukan. Selain itu, saya masih tidak bisa berbicara tentang metaclasses, seperti Herb Sutter , atau introspeksi, seperti Alexandrescu.


Apa yang kita maksudkan ketika berbicara tentang konsep dalam C ++ 20? Fitur ini telah dibahas setidaknya sejak tahun 2003, dan selama ini telah berhasil berkembang pesat. Mari kita lihat fitur-fitur terkait konsep baru apa yang muncul di C ++ 20.


Entitas baru yang disebut "konsep" didefinisikan oleh kata kunci concept . Ini adalah predikat pada parameter templat. Itu terlihat seperti ini:


 template <typename T> concept NoThrowDefaultConstructible = noexept(T{}); template <typename From, typename To> concept Assignable = std::is_assignable_v<From, To> 

Saya tidak hanya menggunakan frasa "pada parameter templat", dan bukan "pada tipe", karena konsep dapat didefinisikan pada parameter templat non-standar. Jika tidak ada yang bisa dilakukan sama sekali, Anda dapat mendefinisikan konsep untuk nomor:


 template<int I> concept Even = I % 2 == 0; 

Tetapi lebih masuk akal untuk mencampur parameter template yang tipikal dan atipikal. Kami memanggil jenis kecil jika ukuran dan perataannya tidak melebihi batas yang ditentukan:


 template<typename T, size_t MaxSize, size_t MaxAlign> concept Small = sizeof(T) <= MaxSize && alignof(T) <= MaxAlign; 

Mungkin, belum jelas mengapa kita perlu memagari entitas baru dalam bahasa, dan mengapa konsepnya bukan hanya variabel constexpr bool .


 //  `concept`    ? #define concept constexpr bool 

Bagaimana konsep digunakan?


Untuk memahami, mari kita lihat bagaimana konsep digunakan.


Pertama, seperti constexpr bool variabel constexpr bool , mereka dapat digunakan di mana pun Anda membutuhkan ekspresi boolean dalam waktu kompilasi. Misalnya, di dalam static_assert atau di dalam noexcept
spesifikasi:


 // bool expression evaluated in compile-time static_assert(Assignable<float, int>); template<typename T> void test() noexcept(NothrowDefaultConstructible<T>) { T t; ... } 

Kedua, konsep dapat digunakan alih-alih nama typename atau kata kunci class saat mendefinisikan parameter template. Tentukan kelas optional sederhana yang hanya akan menyimpan sepasang flag boolean yang initialized dan nilai-nilai. Tentu saja, optional semacam itu hanya berlaku untuk tipe sepele. Oleh karena itu, kami menulis Trivial sini dan ketika kami mencoba untuk instantiate dari sesuatu yang non-trivial, misalnya, dari std::string , kami akan memiliki kesalahan kompilasi:


 //  type-parameter-key (class, typename) template<Trivial T> class simple_optional { T value; bool initialized = false; ... }; 

Konsep dapat diterapkan sebagian. Sebagai contoh, kami mengimplementasikan kelas kami dengan optimasi buffer kecil. Tentukan struktur SB (buffer kecil) dengan Size dan Alignment tetap, kami akan menyimpan serikat dari SB dan pointer pada heap. Dan sekarang, jika tipe kecil masuk ke konstruktor, maka kita bisa memasukkannya ke dalam SB . Untuk menentukan bahwa suatu jenis kecil, kami menulis bahwa itu memenuhi konsep Small . Konsep Small mengambil 3 parameter template: kami mendefinisikan dua, dan kami mendapat fungsi dari satu parameter template:


 //   class any { struct SB { static constexpr size_t Size = ...; static constexpr size_t Alignment = ...; aligned_storage_t<Size, Alignment> storage; }; union { SB sb; void* handle; }; template<Small<SB::Size, SB::Alignment> T> any(T const & t) : sb(...) ... }; 

Ada catatan yang lebih pendek. Kami menulis nama parameter templat, mungkin dengan beberapa argumen, sebelum auto . Contoh sebelumnya ditulis ulang dengan cara ini:


 // Terse syntax (  auto) class any { struct SB { static constexpr size_t Size = ...; static constexpr size_t Alignment = ...; aligned_storage_t<Size, Alignment> storage; }; union { SB sb; void* handle; }; any(Small<SB::Size, SB::Alignment> auto const & t) : sb(...) ... }; 

Mungkin, di tempat mana pun kami menulis auto , sekarang Anda dapat menulis nama konsep di depannya.


Tentukan fungsi get_handle , yang mengembalikan beberapa handle untuk objek.
Kita mengasumsikan bahwa objek kecil itu sendiri adalah handle , dan untuk objek besar, sebuah penunjuk pada mereka adalah handle . Karena kita memiliki dua cabang if constexpr menunjukkan ekspresi dari tipe yang berbeda, akan lebih mudah bagi kita untuk tidak menentukan tipe fungsi ini secara eksplisit, tetapi meminta kompiler untuk menampilkannya. Tetapi jika kita hanya auto , kita akan kehilangan informasi bahwa nilai yang ditunjukkan kecil, itu tidak melebihi pointer:


 //Terse syntax (  auto) template<typename T> concept LEPtr = Small<T, sizeof(void *), alignof(void *)>; template<typename T> auto get_handle(T& object) { if constexpr (LEPtr<T>) return object; else return &object; } 

Dalam C ++ 20, dimungkinkan untuk menulis sebelumnya bahwa itu bukan hanya auto , tetapi auto terbatas:


 // Terse syntax (  auto) template<typename T> concept LEPtr = Small<T, sizeof(void *), alignof(void *)>; template<typename T> LEPtr auto get_handle(T &object) { if constexpr (LEPtr<T>) return object; else return &object; } 

Membutuhkan ekspresi


Membutuhkan ekspresi adalah seluruh keluarga ekspresi'ov, semuanya bertipe bool dan dihitung dalam waktu kompilasi. Mereka digunakan untuk menguji pernyataan tentang ekspresi dan tipe. Membutuhkan ekspresi sangat berguna untuk mendefinisikan konsep.


Contoh yang Constructible . Mereka yang ada di laporan saya sebelumnya sudah melihatnya:


 template<typename T, typename... Args> concept Constructible = requires(Args... args) { T{args...} }; 

Dan contoh dengan Comparable . Katakanlah tipe T adalah Comparable jika dua objek tipe T dapat dibandingkan dengan menggunakan operator "kurang" dan hasilnya dikonversi menjadi bool . Panah ini dan tipe setelahnya berarti bahwa ekspresi tipe dikonversi menjadi bool , dan tidak sama dengan bool :


 template<typename T> concept Comparable = requires(T const & a, T const & b) { {a < b} -> bool; }; 

Apa yang kami kaji sudah cukup untuk menunjukkan contoh penggunaan konsep secara penuh.


Kami sudah memiliki konsep Comparable , mari kita mendefinisikan konsep untuk iterator. Katakanlah RandomAccessIterator adalah BidirectionalIterator dan beberapa properti lainnya. Dengan ini, kami mendefinisikan konsep Sortable . Range disebut Sortable jika iterator RandomAccess dan elemen-elemennya dapat dibandingkan. Dan sekarang kita dapat menulis fungsi sort yang menerima tidak hanya itu, tetapi Sortable Range :


 // concepts,    ++20 template<typename Iterator> concept RandomAccessIterator = BidirectionalIterator<Iterator> && ...; template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<Sortable Range> void sort(Range &) {...} 

Sekarang, jika kita mencoba memanggil fungsi ini dari sesuatu yang tidak memenuhi konsep Sortable , kita akan mendapatkan kesalahan ramah SFINAE yang bagus dari kompiler dengan pesan yang jelas. Mari kita coba instantiate std::list 'atau vektor elemen yang tidak bisa dibandingkan:


 //concepts,    ++20,  struct X {}; void test() { vector<int> vi; sort(vi); // OK list <int> li; sort(li); // Fail, list<int>::iterator is not random access vector< X > vx; sort(vx); // Fail, X is not Comparable } 

Pernahkah Anda melihat contoh serupa menggunakan konsep atau sesuatu yang sangat mirip? Saya telah melihat ini beberapa kali. Jujur, itu tidak meyakinkan saya sama sekali. Apakah kita perlu memagari begitu banyak entitas baru dalam bahasa, jika kita bisa mendapatkan ini dalam C ++ 17?


 //concepts,    ++17 #define concept constexpr bool template<typename T> concept Comparable = is_convertible_v< decltype(declval<T const &>() < declval<T const &>()), bool >; template<typename Iterator> concept RandomAccessIterator = BidirectionalIterator<Iterator> && ...; template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range, typename = enable_if_t<Sortable<Range>>> void sort(Range &) { ... } 

Saya memasukkan concept kata kunci concept makro, dan Comparable ditulis ulang dengan cara ini. Ini menjadi sedikit lebih buruk, dan ini mengisyaratkan kepada kita bahwa menuntut ekspresi benar-benar bermanfaat dan nyaman. Jadi kami mendefinisikan konsep Sortable dan menggunakan enable_if mengindikasikan bahwa fungsi sort menerima Sortable Range .


Anda mungkin berpikir bahwa metode ini kehilangan banyak sesuai dengan pesan kesalahan kompilasi, tetapi, pada kenyataannya, ini adalah masalah kualitas implementasi kompiler. Katakanlah bahwa Dentang membuat keributan tentang topik ini dan secara khusus melompat bahwa jika Anda mengganti enable_if Anda memiliki argumen pertama
Jika false dihitung, maka mereka menyajikan kesalahan ini sehingga persyaratan seperti itu tidak terpenuhi.


Contoh di atas tampaknya ditulis melalui konsep. Saya punya hipotesis: contoh ini tidak dapat disimpulkan, karena tidak menggunakan fitur utama konsep - memerlukan klausa.


Membutuhkan klausa


Membutuhkan klausa adalah hal yang tergantung pada hampir semua deklarasi templat atau pada fungsi non-templat. Secara sintaksis, ini seperti requires kata kunci, diikuti oleh beberapa ekspresi Boolean. Ini diperlukan untuk memfilter kandidat templat spesialisasi atau kelebihan beban, yaitu, ia bekerja dengan cara yang sama seperti SFINAE, hanya dilakukan dengan benar, dan bukan dengan peretasan:


 // requires-clause template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<Sortable Range> void sort(Range &) { ... } 

Di mana dalam contoh kami yang diurutkan dapat kami gunakan membutuhkan klausa? Alih-alih sintaks singkat untuk menerapkan konsep, kami menulis ini:


 template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range> requires Sortable<Range> void sort(Range &) { ... } 

Tampaknya kode semakin memburuk dan semakin besar. Tapi sekarang kita bisa menyingkirkan konsep Sortable . Dari sudut pandang saya, ini merupakan peningkatan, karena konsep Sortable tautologis: kami menyebut Sortable segala sesuatu yang dapat diteruskan ke fungsi sort . Ini tidak memiliki arti fisik. Kami menulis ulang kode dengan cara ini:


 //template<typename R> concept Sortable // = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range> requires RandomAccessIterator<Iterator<Range>> && Comparable<ValueType<Range>>; void sort(Range &) { ... } 

Daftar ringkasan fitur terkait konsep


Daftar inovasi terkait konsep di C ++ 20 terlihat seperti ini. Item dalam daftar ini disortir dengan meningkatkan utilitas fitur dari sudut pandang subjektif saya:


  • concept entitas baru. Sepertinya saya bahwa itu akan mungkin dilakukan tanpa esensi concept dengan menganugerahkan variabel constexpr bool dengan semantik tambahan.
  • Sintaks khusus untuk menerapkan konsep. Tentu saja, itu menyenangkan, tetapi ini hanya sintaksisnya. Jika pemrogram C ++ takut sintaksis yang buruk, mereka akan mati karena ketakutan sejak lama.
  • Membutuhkan ekspresi benar-benar hal yang keren, dan berguna tidak hanya untuk mendefinisikan konsep.
  • Membutuhkan klausa adalah nilai konsep terbesar, memungkinkan Anda untuk melupakan SFINAE dan kengerian temporer C ++ lainnya yang legendaris.

Lebih lanjut tentang membutuhkan ekspresi


Sebelum kita masuk ke diskusi tentang membutuhkan klausa, beberapa kata tentang memerlukan ekspresi.


Pertama, mereka dapat digunakan tidak hanya untuk mendefinisikan konsep. Sejak dahulu kala, kompiler Microsoft memiliki ekstensi __if_exists - __if_not_exists . Ini memungkinkan waktu kompilasi untuk memverifikasi keberadaan nama dan, tergantung pada ini, mengaktifkan atau menonaktifkan kompilasi blok kode. Dan di basis kode, yang saya gunakan beberapa tahun yang lalu, itu adalah sesuatu seperti ini. Ada fungsi f() , ia mengambil titik dari tipe template dan mengambil ketinggian dari titik ini. Ini bisa dipakai oleh titik tiga dimensi atau dua dimensi. Untuk tiga dimensi, kami menganggap koordinat z sebagai tinggi, untuk dua dimensi, kami beralih ke sensor permukaan khusus. Ini terlihat seperti ini:


 struct Point2 { float x, y; }; struct Point3 { float x, y, z; }; template<typename Point> void f(Point const & p) { float h; __if_exists(Point::z) { h = pz; } __if_not_exists(Point::z) { h = sensor.get_height(p); } } 

Di C ++ 20, kita dapat menulis ulang ini tanpa menggunakan ekstensi kompiler menggunakan kode standar. Bagi saya sepertinya tidak menjadi lebih buruk:


 struct Point2 { float x, y; }; struct Point3 { float x, y, z; }; template<typename Point> void f(Point const & p) { float h; if constexpr(requires { Point::z; }) h = pz; else h = sensor.get_height(p); } 

Poin kedua adalah bahwa Anda harus waspada dengan sintaks yang membutuhkan ekspresi.
Mereka cukup kuat, dan kekuatan ini dicapai dengan diperkenalkannya banyak konstruksi sintaksis baru. Anda bisa bingung di dalamnya, setidaknya pada awalnya.


Mari kita mendefinisikan konsep yang Sizable yang memeriksa bahwa wadah memiliki size metode konstan yang mengembalikan size_t . Kami tentu berharap bahwa vector<int> cukup Sizable , namun static_assert ini static_assert . Apakah Anda mengerti mengapa kami melakukan kesalahan? Mengapa kode ini tidak dikompilasi?


 template<typename Container> concept Sizable = requires(Container const & c) { c.size() -> size_t; }; static_assert(Sizable<vector<int>>); // Fail 

Biarkan saya menunjukkan kode yang mengkompilasi. Kelas X seperti itu memuaskan konsep Sizable . Sekarang Anda mengerti apa yang kita punya masalah?


 struct X { struct Inner { int size_t; }; Inner* size() const; }; static_assert(Sizable<X>); // OK 

Biarkan saya memperbaiki penyorotan kode. Di sebelah kiri, kode ini berwarna sesuai keinginan saya. Tetapi pada kenyataannya, itu harus dicat seperti di sebelah kanan:



Lihat, warna size_t , berdiri setelah panah, telah berubah? Saya ingin menjadi tipe, tetapi hanya bidang yang kami akses. Segala sesuatu yang kita miliki memerlukan ekspresi adalah satu ekspresi besar, dan kami memeriksa kebenarannya. Untuk tipe X , ya, ini adalah ekspresi yang valid; untuk vector<int> , tidak. Untuk mencapai apa yang kita inginkan, kita perlu mengambil ekspresi dalam kurung:


 template<typename Container> concept Sizable = requires(Container const & c) { {c.size()} -> size_t; }; static_assert(Sizable<vector<int>>); // OK struct X { struct Inner { int size_t; }; Inner* size() const; }; static_assert(Sizable<X>); // Fail 

Tapi ini hanya contoh yang menyenangkan. Secara umum, Anda hanya perlu berhati-hati.


Contoh menggunakan konsep


Implementasi kelas berpasangan


Selanjutnya saya akan menunjukkan beberapa fragmen STL yang dapat diimplementasikan dalam C ++ 17, tetapi agak rumit.
Dan kemudian kita akan melihat bagaimana dalam C ++ 20 kita dapat meningkatkan implementasi.


Mari kita mulai dengan kelas pair .
Ini adalah kelas yang sangat lama, masih dalam C ++ 98.
Tidak mengandung logika yang rumit, jadi
Saya ingin definisinya terlihat seperti ini.
Dari sudut pandang saya, kira-kira harus diakhiri dengan ini:


 template<typename F, typename S> struct pair { F f; S s; ... }; 

Tapi, menurut cppreference , pair desainer hanya punya 8 buah.
Dan jika Anda melihat implementasi nyata, misalnya, di Microsoft STL, maka akan ada sebanyak 15 konstruktor dari kelas pair . Kami tidak akan melihat semua kekuatan ini dan membatasi diri pada konstruktor default.


Tampaknya itu adalah sesuatu yang rumit? Untuk memulainya, kami mengerti mengapa itu diperlukan. Kami ingin jika salah satu argumen dari kelas pair adalah tipe sepele, katakan int , maka setelah membangun kelas pair itu diinisialisasi ke nol, dan tidak tetap tidak diinisialisasi. Untuk melakukan ini, kami ingin menulis konstruktor yang memanggil inisialisasi nilai untuk bidang f (pertama) dan s (kedua).


 template<typename F, typename S> struct pair { F f; S s; pair() : f() , s() {} }; 

Sayangnya, jika kami mencoba untuk instantiate pair dari sesuatu yang tidak memiliki konstruktor default, katakanlah, dari kelas , kami segera mendapatkan kesalahan kompilasi. Perilaku yang diinginkan adalah bahwa jika Anda mencoba membangun pair , defaultnya adalah kesalahan kompilasi, tetapi jika kami secara eksplisit melewatkan nilai f dan s , maka semuanya akan berfungsi:


 struct A { A(int); }; pair<int, A> a2; // must fail pair<int, A> a1; { 1, 2 }; // must be OK 

Untuk melakukan ini, buat templat konstruktor default dan batasi pada SFINAE.
Ide pertama yang muncul di pikiran adalah mari kita menulis sehingga konstruktor ini hanya diperbolehkan jika f dan s is_default_constructable :


 template<typename F, typename S> struct pair { F f; S s; template<typename = enable_if_t<conjunction_v< is_default_constructible<F>, // not dependent is_default_constructible<S> >>> pair() : f(), s() {} }; 

Ini tidak akan berfungsi, karena argumen enable_if_t hanya bergantung pada parameter templat kelas. Artinya, setelah substitusi kelas, mereka menjadi mandiri, mereka dapat segera dihitung. Tetapi jika kita false , masing-masing, kita kembali mendapatkan kesalahan kompiler keras.


Untuk mengatasinya, mari kita tambahkan lebih banyak parameter templat ke konstruktor ini dan buat ketentuan enable_if_t bergantung pada parameter templat ini:


 template<typename F, typename S> struct pair { F f; S s; template<typename T = F, typename U = S, typename = enable_if_t<conjunction_v< is_default_constructible<T>, is_default_constructible<U> >>> pair() : f(), s() {} }; 

Situasinya sangat lucu. Faktanya adalah bahwa parameter template T dan U tidak dapat diatur secara eksplisit oleh pengguna. Dalam C + +, tidak ada sintaks untuk secara eksplisit mengatur parameter template dari konstruktor, mereka tidak dapat di-output oleh compiler, karena tidak ada tempat untuk menampilkannya. Mereka hanya bisa datang dari nilai default. Artinya, secara efektif kode ini tidak berbeda dengan kode pada contoh sebelumnya. Namun, dari sudut pandang kompiler, itu valid, tetapi tidak dalam contoh sebelumnya.


Kami memecahkan masalah pertama kami, tetapi kami dihadapkan dengan yang kedua, sedikit lebih halus. Misalkan kita memiliki kelas B dengan konstruktor default eksplisit, dan kami ingin secara implisit membangun pair<int, B> :


 struct B { explicit B(); }; pair<int, B> p = {}; 

Kita bisa melakukannya, tetapi, menurut standar, itu tidak akan berhasil. Secara standar, suatu pasangan harus secara default secara implisit dibangun hanya jika kedua elemennya secara default secara default dibangun.


Pertanyaan: apakah kita perlu menulis konstruktor dari pasangan eksplisit atau tidak? Dalam C ++ 17, kami memiliki solusi Solomon: mari kita tulis ini dan itu.


 template<typename F, typename S> struct pair { F f; S s; template<typename T = F, typename U = S, typename = enable_if_t<conjunction_v< is_default_constructible<T>, is_default_constructible<U>, is_implicity_default_constructible<T>, is_implicity_default_constructible<U> >>> pair() : f(), s() {} template<...> explicit pair() : f(), s() {} }; 

Sekarang kami memiliki dua konstruktor default:


  • kami akan memotong salah satu dari mereka sesuai dengan SFINAE untuk kasus ketika unsur-unsur secara implisit standar dibangun;
  • dan yang kedua untuk kasus sebaliknya.

Omong-omong, untuk mengimplementasikan tipe sifat is_implicitly_default_constructible di C ++ 17, saya tahu solusi seperti itu, tetapi saya tidak tahu solusinya tanpa SFINAE:


 template<typrname T> true_type test(T, int); template<typrname T> false_type test(int, ...); template<typrname T> using is_implicity_default_constructible = decltype(test<T>({}, 0)); 

Jika kita sekarang mencoba membangun pair <int, B> secara implisit, maka kita mendapatkan kesalahan kompilasi, seperti yang kita inginkan:


 template<..., typename = enable_if_t<conjuction_v< is_default_constructible<T>, is_default_constructible<U>, is_implicity_default_constructible<T>, is_implicity_default_constructible<U> >>> ... pair<int, B> p = {}; ... candidate template ignored: requirement 'conjunction_v< is_default_constructible<int>, is_default_constructible<B>, is_implicity_default_constructible<int>, is_implicity_default_constructible<B> >' was not satisfied [with T=int, U=B] 

Dalam kompiler yang berbeda, kesalahan ini akan memiliki berbagai tingkat kelayakan. Sebagai contoh, kompiler Microsoft dalam kasus ini mengatakan: "Itu tidak mungkin untuk membangun pasangan <int, B> dari kurung keriting kosong." GCC dan Clang akan menambahkan ini: "Kami mencoba konstruktor ini dan itu, tidak satu pun dari mereka muncul," dan mereka akan mengatakan alasan tentang masing-masing.


Desainer apa yang kita miliki di sini? Ada konstruktor yang dihasilkan oleh copy dan move compiler, ada beberapa yang ditulis oleh kami. Dengan menyalin dan memindahkan, semuanya sederhana: mereka mengharapkan satu parameter, kami melewati nol. Untuk konstruktor kami, alasannya adalah substitusinya floppy.


GCC mengatakan: "Pergantian gagal, mencoba menemukan jenis type di dalam enable_if<false> - tidak dapat menemukan, maaf."


Dentang menganggap situasi ini sebagai kasus khusus. Karena itu, ia sangat keren menunjukkan kesalahan ini. Jika kita false ketika mengevaluasi enable_if argumen pertama, dia menulis bahwa persyaratan spesifik tidak terpenuhi.


Pada saat yang sama, kita sendiri merusak hidup kita dengan membuat kondisi yang rumit memungkinkan. Kami melihat bahwa itu ternyata false , tetapi kami belum melihat mengapa.


Ini dapat diatasi jika kita memecah enable_if menjadi empat dengan cara ini:


 template<..., typename = enable_if_t<is_default_constructible<T>::value>>, typename = enable_if_t<is_default_constructible<U>::value>>, typename = enable_if_t<is_implicity_default_constructible<T>::value>>, typename = enable_if_t<is_implicity_default_constructible<U>::value>> > ... 

Sekarang, ketika kami mencoba membangun pasangan secara implisit, kami mendapatkan pesan yang sangat bagus bahwa kandidat semacam itu tidak cocok, karena jenis sifat is_implicitly_default_constructable tidak puas:


 pair<int, B> p = {}; // candidate template ignored: requirement 'is_implicity_default_constructible<B>::value' was not satisfied with... 

Bahkan mungkin tampak sebentar: mengapa kita perlu konsep jika kita memiliki kompiler yang keren?
Tapi kemudian kita ingat bahwa dua fungsi templat digunakan secara default untuk mengimplementasikan konstruktor, dan masing-masing templat memiliki enam parameter templat. Untuk bahasa yang mengklaim dirinya kuat, ini adalah kegagalan.


Bagaimana C ++ 20 akan membantu kami? Pertama, singkirkan pola dengan menulis ulang ini dengan membutuhkan klausa. Apa yang sebelumnya kita tulis di dalam enable_if , sekarang kita menulis di dalam argumen memerlukan klausa:


 template<typename F, typename S> struct pair { F f; S s; pair() requires DefaultConstructible<F> && DefaultConstructible<S> && ImplicitlyDefaultConstructible<F> && ImplicitlyDefaultConstructible<S> : f(), s() {} explicit pair() ... }; 

Konsep ImplicitlyDefaultConstructible dapat diimplementasikan menggunakan ekspresi membutuhkan yang bagus, yang di dalamnya hampir hanya kurung dengan bentuk yang berbeda digunakan:


 template<typename T> concept ImplicitlyDefaultConstructible = requires { [] (T) {} ({}); }; 

T ImplicitlyDefaultConstructible , , T . , , SFINAE.


C++20: (conditional) explicit ( noexcept ). explicit . , explicit .


 template<typename F, typename S> struct pair { F f; S s; explicit(!ImplicityDefaultConstructible<F> || !ImplicityDefaultConstructible<S>) pair() requires DefaultConstructible<F> && DefaultConstructible<S> : f(), s() {} }; 

, . , DefaultConstructible , explicit , explicit .


Optional C++17


Optional . , .


. ? , C++ :


 enum Option<T> { None, Some(t) } 

:


 class Optional<T> { final T value; Optional() {this.value = null; } Optional(T value) {this.value = value; } } 

C++: null , value-?


C++ . initialized storage , , . T , optional T , C++ memory model.


 template<typename T> class optional { bool initialized; aligned_storage_t<sizeof(T), alignof(T)> storage; ... 

, . : optional , optional . :


  ... T & get() & { return reinterpret_cast<T &>(storage); } T const & get() const & { return reinterpret_cast<T const &>(storage); } T && get() && { return move(get()); } optional() noexcept : initialized(false) {} optional(T const & value) noexcept(NothrowCopyConstructible<T>) : initialized(true) { new (&storage) T(value); } ~optional() : noexcept(NothrowDestructible<T>) { if (initialized) get().~T(); } }; 

optional ' . optional , optional , , optional . , copy move .


. : assignment . , . . copy constructor. :


 template<typename T> class optional { bool initialized; aligned_storage_t<sizeof(T), alignof(T)> storage; ... optional(optional const & other) noexcept(NothrowCopyConstructible<T>) : initialized(other.initialized) { if (initialized) new (&storage) T(other.get()); } optional& operator =(optional && other) noexcept(...) {...} }; 

move assignment. , :


  • optional ' , .
  • , .
  • , β€” , , .

T : move constructor, move assignment :


 optional& operator =(optional && other) noexcept(...) { if (initialized) { if (other.initialized) { get() = move(other.get()); } else { initialized = false; other.initilized = true; new(&other.storage) T(move(get())); get().~T(); } } else if (other.initialized) { initialized = true; other.initialized = false; new(&storage) T(move(get())); other.get().~T(); } return *this; } 

noexcept :


 optional& operator =(optional && other) noexcept(NothrowAssignable<T> && NothrowMoveConstructible<T> && NothrowDestructible<T>) { if (initialized) { if (other.initialized) { get() = move(other.get()); } else { initialized = false; other.initialized = true; new (&other.storage) T(move(get())); get().~T(); } } ... } 

optional :


 template<typename T> class optional { ... optional(optional const &) noexcept(NothrowCopyConstructible<T>); optional(optional &&) noexcept(NothrowMoveConstructible<T>); optional& operator =(optional const &) noexcept(...); optional& operator =(optional &&) noexcept(...); }; 

, pair :
Optional -, (, deleted), compilation error.


 template class optional<unique_ptr<int>>; // compilation error 

, optional unique_ptr ,
copy constructor copy assignment deleted. , , SFINAE.
copy move assignment , β€” . - , copy , .


β€” . copy : deleted operation , , operation:


  • deleted_copy_construct delete , β€” default ;
  • copy_construct , copy_construct .

 template<class Base> struct deleted_copy_construct : Base { deleted_copy_construct(deleted_copy_construct const &) = delete; deleted_copy_construct(deleted_copy_construct &&) = default; deleted_copy_construct& operator =(deleted_copy_construct const &) = default; deleted_copy_construct& operator =(deleted_copy_construct &&) = default; }; template<class Base> struct copy_construct : Base { copy_construct(copy_construct const & other) noexcept(noexcept(Base::construct(other))) { Base::construct(other); } copy_construct(copy_construct &&) = default; copy_construct& operator =(copy_construct const &) = default; copy_construct& operator =(copy_construct &&) = default; }; 

select_copy_construct , , CopyConstrictuble , copy_construct , deleted_copy_construct :


 template<typename T, class Base> using select_copy_construct = conditional_t<CopyConstructible<T> copy_construct<Base> deleted_copy_construct<Base> >; 

, optional , optional_base , copy construct , optional
select_copy_construct<T, optional_base<T>> . copy :


 template<typename T> class optional_base { ... void construct(optional_base const & other) noexcept(NothrowCopyConstructible<T>) { if ((initialized = other.initialized)) new (&storage) t(other.get()); } }; template<typename T> class optional : select_copy_construct<T, optional_base<T>> { ... }; 

. , , copy_construct , move_construct copy_construct , copy_assign , , move_construct , , , :


 template<typename T, class Base> using select_move_construct = select_copy_construct<T, conditional_t<MoveConstructible<T>, move_construct<Base> > >; template<typename T, class Base> using select_copy_assign = select_move_construct<T, conditional_t<CopyAssignable<T> && CopyConstructible<T>, copy_assign<Base> delete_copy_assign<Base> > >; 

, move_assign copy_assign , optional_base , assignment construct assign , optional select_move_assign<T, optional_base<T>> .


 template<typename T, class Base> using select_move_assign = select_copy_assign<T, ...>; template<typename T> class optional_base { ... void construct(optional_base const&) noexcept(NothrowCopyConstructible<T>); void construct(optional_base &&) noexcept(NothrowMoveConstructible<T>); optional_base& assign(optional_base &&) noexcept(...); optional_base& assign(optional_base const &) noexcept(...); }; template<typename T> class optional : select_move_assign<T, optional_base<T>> { ... }; 

, :
optional<unique_ptr> deleted_copy_construct ,
move_construct . !


 optional<unique_ptr<int>> : deleted_copy_construct<...> : move_construct<...> : deleted_copy_assign<...> : move_assign<...> : optional_base<unique_ptr<int>> 

: optional TriviallyCopyable TriviallyCopyable .


TriviallyCopyable ? , T TriviallyCopyable ,
memcpy . , .


, , , . resize vector TriviallyCopyable , memcpy , , . , , .


TriviallyCopyable , , static_assert ', copy-move :


 template<typename T> class optional : select_move_assign<T, optional_base<T>> {...}; static_assert(TriviallyCopyable<optional<int>>); static_assert(TriviallyCopyConstructible<optional<int>>); static_assert(TriviallyMoveConstructible<optional<int>>); static_assert(TriviallyCopyAssignable <optional<int>>); static_assert(TriviallyMoveAssignable <optional<int>>); static_assert(TriviallyDestructible <optional<int>>); 

static_assert ' . , , . optional β€” aligned_storage , , , , TriviallyCopyable .


, . , TriviallyCopyable .


, . select_copy_construct :


 template<typename T, class Base> using select_copy_construct = conditional_t<CopyConstructible<T>, copy_construct<Base> deleted_copy_construct<Base> >; 

CopyContructible copy_construct , if compile-time: CopyContructible TriviallyCopyContructible , Base .


 template<typename T, class Base> using select_copy_construct = conditional_t<CopyConstructible<T>, conditional_t<TriviallyCopyConstructible<T>, Base, copy_construct<Base> >, deleted_copy_construct<Base> >; 

, copy . , select_destruct . int , - - , .


 template<typename T, class Base> using select_destruct = conditional_t<TriviallyDenstructible<T>, Base, destruct<Base> > >; 

, , . , , :


 optional<unique_ptr<int>> : deleted_copy_construct<...> : move_construct<...> : deleted_copy_assign<...> : move_assign<...> : destruct<optional_base<unique_ptr<int>>> : optional_base<unique_ptr<int>> 

, C++17 optional 7; : operation , deleted_operation select_operation ; construct assign . , .


- . . : deleted.


, noexcept .
, , , trivial , noexcept . , , trivial noexcept , noexcept , deleted . . , , .


type trait, , . , , copy : deleted , nothrow , ?


, - special member, , , , :


  • , deleted , = delete deleted_copy_construct ;
  • , copy_construct , c noexcept ;
  • , , , .

.


optional C++20


C++20 optional copy ?
:


  • T CopyConstructible , deleted ;
  • TriviallyCopyConstructible , ;
  • noexcept .

 template<typename T> class optional { ... optional(optional const &) requires(!CopyConstructible<T>) = delete; // #1 optional(optional const &) requires(TriviallyCopyConstructible<T>) = default; // #2 optional(optional const &) noexcept(NothrowCopyConstructible<T>) {...} // #3 ... ~optional() requires(TriviallyDestructible<T>) = default; ~optional() noexcept(NothroeDestructible<T>) {...} }; 

, . -, , T requires clause false . requires(false) , , overload resolution. , requires(true) , .
, .


requires clause = delete :


  • = delete overload resolution, , , deleted .
  • requires(false) overload resolution.

, copy , , requires clause. .


, . ! C++ , ? , , . , , , . , , , , , optional .


, , GCC internal compiler error, Clang . , . , .


, , optional C++20. , , C++17.


aligned_storage aligned_union


: aligned_storage reinterpret_cast , reinterpret_cast constexpr . , compile-time optional , compile-time. STL aligned_storage optional aligned_union variant . , , STL Boost optional variant . variant , :


 template<bool all_types_are_trivially_destructible, typename...> class _Variant_storage_; template<typename... _Types> using _Variant_storage = _Variant_storage_< conjunction_v<is_trivially_destructible<_Types>...>, _Types... >; template<typename _First, typename... _Rest> class _Variant_storage_<true, _First, _Rest...> { union { remove_const_t<First> _Head; _Variant_storage<_Rest...> _Tail; }; }; 

variant . _Variant_storage_ , , -, , variant , -, . , trivially_destructible ? type alias, . _Variant_storage_ , true false . , true , . trivially_destructible , union Variant ' .


, , , , . type alias _Variant_storage . :


 template<typename... _Types, bool = conjunction_v<is_trivially_destructible<_Types>...> > class _Variant_storage_; 

. , variadic template . , , , _Types . C++17 , .


C++20 ,
,
requires clause. C++20 requires clause:


 template<typename... _Types> class _Variant_storage_; template<typename _First, typename... _Rest> requires(TriviallyDestructible<_First> && ... && TriviallyDestuctible<_Rest>) class _Variant_storage_<_First, _Rest...> { union { remove_const_t<_First> _Head; _Variant_storage_<_Rest...> _Tail }; }; 

_Variant_storage_ , TriviallyDestructible . , requires clause , , .


requires clause template type alias


, requires clause template type alias. C++20 - enable_if , :


 template<bool condition, typename T = void> requires condition using enable_if_t = T; 

,


, . :


 // Equivalent, but functionally not equivalent template<typename T> enable_if_t<(sizeof(T) < 239)> f(); template<typename T> enable_if_t<(sizeof(T) > 239)> f(); // Not equivalent template<typename T> requires(sizeof(T) < 239) void f(); template<typename T> requires(sizeof(T) > 239) void f(); 

, enable_if . ? f() : enable_if , , 239, , , , 239. , :


  • , , template type alias', Β«void f(); void f();
  • , SFINAE, , , .

, enable_if , , size < 239 , size > 239 . , . , f() . requires clause. β€” , .


β€” , . C++ Russia 2019 Piter, Β«: core languageΒ» . , , : reachable entity visible, ADL, entities internal linkage . , C++ Russia (JetBrains) Β« ++20 β€” ?Β»

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


All Articles