
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
.
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:
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:
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:
Ada catatan yang lebih pendek. Kami menulis nama parameter templat, mungkin dengan beberapa argumen, sebelum auto
. Contoh sebelumnya ditulis ulang dengan cara ini:
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:
Dalam C ++ 20, dimungkinkan untuk menulis sebelumnya bahwa itu bukan hanya auto
, tetapi auto
terbatas:
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
:
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:
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?
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:
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 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>>);
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>);
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>>);
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;
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>,
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 = {};
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>>;
, 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;
, . -, , 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;
,
, . :
, 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 β ?Β»