Kesan pertama konsep



Saya memutuskan untuk berurusan dengan fitur - konsep C ++ 20 yang baru.

Konsep (atau konsep , seperti yang ditulis Wiki berbahasa Rusia) adalah fitur yang sangat menarik dan bermanfaat yang telah lama kurang.

Pada dasarnya, ini mengetik argumen templat.

Masalah utama templat sebelum C ++ 20 adalah Anda dapat mengganti apa pun di dalamnya, termasuk sesuatu yang tidak dirancang sama sekali. Yaitu, sistem templat sepenuhnya tidak diketik. Akibatnya, pesan kesalahan yang sangat panjang dan benar-benar tidak dapat dibaca terjadi ketika memberikan parameter yang salah ke templat. Mereka mencoba melawan ini dengan bantuan peretas bahasa yang berbeda, yang bahkan tidak ingin saya sebutkan (walaupun saya harus berurusan dengan).

Konsep dirancang untuk memperbaiki kesalahpahaman ini. Mereka menambahkan sistem pengetikan ke template, dan ini sangat kuat. Dan sekarang, setelah memahami fitur-fitur sistem ini, saya mulai mempelajari materi yang tersedia di Internet.

Terus terang, saya sedikit kaget :) C ++ adalah bahasa yang sudah rumit, tapi setidaknya ada alasan: itu terjadi. Metaprogramming pada templat ditemukan, tidak diletakkan, ketika merancang bahasa. Dan kemudian, ketika mengembangkan versi bahasa berikutnya, mereka dipaksa untuk beradaptasi dengan "penemuan" ini, karena banyak kode ditulis di dunia. Konsep adalah peluang yang secara fundamental baru. Dan, menurut saya, beberapa opacity sudah ada dalam implementasinya. Mungkin ini merupakan konsekuensi dari kebutuhan untuk memperhitungkan sejumlah besar kemampuan yang diwarisi? Mari kita coba mencari tahu ...

Informasi Umum


Konsep adalah entitas bahasa baru berdasarkan sintaks templat. Konsep memiliki nama, parameter, dan tubuh - predikat yang mengembalikan nilai konstanta (mis., Dihitung pada tahap kompilasi) tergantung pada parameter konsep. Seperti ini:

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

template<typename T>
concept FourByte = sizeof(T)==4;

Secara teknis, konsep sangat mirip dengan ekspresi template constexpr seperti bool:

template<int I>
constexpr bool EvenX = I % 2 == 0; 

template<typename T>
constexpr bool FourByteX = sizeof(T)==4;

Anda bahkan dapat menggunakan konsep dalam ekspresi umum:

bool b1 = Even<2>; 

Menggunakan


Ide utama dari konsep ini adalah bahwa mereka dapat digunakan sebagai ganti dari nama ketik atau kata kunci kelas dalam template. Seperti metatypes ("types for types"). Jadi, pengetikan statis dimasukkan ke dalam templat.

template<FourByte T>
void foo(T const & t) {}

Sekarang, jika kita menggunakan int sebagai parameter templat, maka kode di sebagian besar kasus akan dikompilasi; dan jika dobel, maka pesan kesalahan pendek dan dapat dimengerti akan dikeluarkan. Pengetikan templat yang sederhana dan jelas, sejauh ini semuanya baik-baik saja.

membutuhkan


Ini adalah kata kunci C ++ 20 β€œkontekstual” baru dengan tujuan ganda: memerlukan klausa dan memerlukan ekspresi. Seperti yang akan ditampilkan nanti, penghematan kata kunci yang aneh ini menyebabkan beberapa kebingungan.

membutuhkan ekspresi


Pertama, pertimbangkan membutuhkan ekspresi. Idenya cukup bagus: kata ini memiliki blok di kurung kurawal, kode di dalamnya dievaluasi untuk kompilasi. Benar, kode di sana tidak boleh ditulis dalam C ++, tetapi dalam bahasa khusus, dekat dengan C ++, tetapi memiliki karakteristik sendiri (ini adalah keanehan pertama, sangat mungkin untuk membuat hanya kode C ++).

Jika kode itu benar - memerlukan ekspresi mengembalikan true, jika tidak, false. Kode itu sendiri, tentu saja, tidak pernah masuk ke dalam pembuatan kode, seperti halnya ekspresi dalam sizeof atau decltype.

Sayangnya, kata tersebut kontekstual dan hanya berfungsi di dalam templat, yaitu di luar templat, ini tidak dapat dikompilasi:

bool b = requires { 3.14 >> 1; };

Dan dalam template - silakan:

template<typename T>
constexpr bool Shiftable = requires(T i) { i>>1; };

Dan itu akan berhasil:

bool b1 = Shiftable<int>; // true
bool b2 = Shiftable<double>; // false

Penggunaan utama ekspresi membutuhkan adalah penciptaan konsep. Misalnya, ini adalah bagaimana Anda bisa memeriksa keberadaan bidang dan metode dalam suatu tipe. Kasus yang sangat populer.

template <typename T>
concept Machine = 
  requires(T m) {  //   `m` ,   Machine
	m.start();     //    `m.start()` 
	m.stop();      //   `m.stop()`
};  

Omong-omong, semua variabel yang mungkin diperlukan dalam kode yang diuji (tidak hanya parameter templat) harus dinyatakan dalam tanda kurung membutuhkan ekspresi. Untuk beberapa alasan, menyatakan suatu variabel sama sekali tidak mungkin.

Ketik memeriksa di dalam membutuhkan


Di sinilah perbedaan antara membutuhkan kode dan standar C ++ dimulai. Untuk memeriksa jenis yang dikembalikan, sintaks khusus digunakan: objek diambil dalam kurung keriting, panah ditempatkan, dan setelah itu, sebuah konsep ditulis bahwa jenis harus memenuhi. Selain itu, penggunaan tipe langsung tidak diperbolehkan.

Periksa bahwa kembalinya fungsi dapat dikonversi ke int:

requires(T v, int i) {
  { v.f(i) } -> std::convertible_to<int>;
}  

Periksa apakah fungsi pengembalian tepat int:

requires(T v, int i) {
  { v.f(i) } -> std::same_as<int>; 
}  

(std :: same_as dan std :: convertible_to adalah konsep dari pustaka standar).

Jika Anda tidak menyertakan ekspresi yang tipenya diperiksa dalam kurung, kompiler tidak mengerti apa yang mereka inginkan darinya dan menafsirkan seluruh string sebagai ekspresi tunggal yang perlu diperiksa untuk kompilasi.

membutuhkan di dalam membutuhkan


Kata kunci yang membutuhkan memiliki arti khusus di dalam membutuhkan ekspresi. Nested membutuhkan-ekspresi (sudah tanpa kurung kurawal) diperiksa bukan untuk kompilasi, tetapi untuk kesetaraan benar atau salah. Jika ungkapan seperti itu ternyata salah, maka ungkapan terlampir segera ternyata salah (dan analisis kompilasi lebih lanjut terganggu). Bentuk umum:

requires { 
  expression;         // expression is valid
  requires predicate; // predicate is true
};

Sebagai predikat, misalnya, konsep atau tipe karakter yang didefinisikan sebelumnya dapat digunakan. Contoh:

requires(Iter it) {
  //     (   Iter   *  ++)
  *it++;
 
  //    -  
  requires std::convertible_to<decltype(*it++), typename Iter::value_type>;
 
  //    -  
  requires std::is_convertible_v<decltype(*it++), typename Iter::value_type>;
}

Pada saat yang sama, ekspresi-kebutuhan bersarang diizinkan dengan kode dalam kurung keriting, yang diperiksa validitasnya. Namun, jika Anda hanya menulis satu ekspresi-kebutuhan di dalam yang lain, maka ekspresi bersarang (semuanya secara keseluruhan, termasuk kata kunci yang diperlukan bersarang) hanya akan diperiksa validitasnya:

requires (T v) { 
  requires (typename T::value_type x) { ++x; }; //     , 
												//     !
};  

Oleh karena itu, bentuk aneh dengan kebutuhan ganda muncul:

requires (T v) { 
  requires requires (typename T::value_type x) { ++x; }; //       "++x"
};  

Berikut ini adalah urutan melarikan diri yang menyenangkan dari "membutuhkan".

Ngomong-ngomong, kombinasi keduanya diperlukan adalah klausa waktu ini (lihat di bawah) dan ekspresi:

template <typename T>
  requires requires(T x, T y) { bool(x < y); }
bool equivalent(T const& x, T const& y)
{
  return !(x < y) && !(y < x);
};

membutuhkan klausa


Sekarang mari kita beralih ke penggunaan kata lain yang diperlukan - untuk menyatakan batasan jenis templat. Ini adalah alternatif untuk menggunakan nama konsep alih-alih nama ketik. Dalam contoh berikut, ketiga metode ini setara:

//  require
template<typename Cont>
	requires Sortable<Cont>
void sort(Cont& container);

//   require (  )
template<typename Cont>
void sort(Cont& container) requires Sortable<Cont>;

//    typename
template<Sortable Cont>
void sort(Cont& container)  

Deklarasi yang mewajibkan dapat menggunakan beberapa predikat yang dikombinasikan oleh operator logis.

template <typename T>
  requires is_standard_layout_v<T> && is_trivial_v<T>
void fun(T v); 
 
int main()
{
  std::string s;
 
  fun(1);  // ok
  fun(s);  // compiler error
}

Namun, balikkan salah satu syarat, saat kesalahan kompilasi terjadi:

template <typename T>
  requires is_standard_layout_v<T> && !is_trivial_v<T>
void fun(T v); 

Berikut adalah contoh yang tidak akan dikompilasi

template <typename T>
  requires !is_trivial_v<T>
void fun(T v);	

Alasan untuk ini adalah ambiguitas yang muncul ketika mengurai beberapa ekspresi. Misalnya, dalam templat seperti itu:

template <typename T> 
  requires (bool)&T::operator short unsigned int foo();

tidak jelas apa atribut yang tidak ditandatangani - operator atau prototipe fungsi foo (). Oleh karena itu, pengembang memutuskan bahwa tanpa tanda kurung hanya petunjuk literal benar atau salah, nama bidang tipe bool dari nilai formulir, nilai, T :: value, ns :: trait :: value, dapat digunakan sebagai argumen yang membutuhkan klausa. Konsep nama dari tipe Konsep dan membutuhkan ekspresi. Segala sesuatu yang lain harus ditutup dalam tanda kurung:

template <typename T>
  requires (!is_trivial_v<T>)
void fun(T v);

Sekarang tentang fitur predikat di membutuhkan klausa


Pertimbangkan contoh lain.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v); 

Dalam contoh ini, mengharuskan menggunakan sifat yang bergantung pada tipe value_type bersarang. Tidak diketahui sebelumnya apakah tipe arbitrer memiliki tipe bersarang yang dapat diteruskan ke templat. Jika Anda melewatkan, misalnya, tipe int sederhana untuk templat seperti itu, akan ada kesalahan kompilasi, namun, jika kami memiliki dua spesialisasi templat, maka tidak akan ada kesalahan; hanya spesialisasi lain yang akan dipilih.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v) { std::cout << "1"; } 
 
template <typename T>
void fun(T v) { std::cout << "2"; } 
 
int main()
{
  fun(1);  // displays: "2"
}

Dengan demikian, spesialisasi tidak hanya dibuang ketika prasyarat klausa klausa mengembalikan false, tetapi juga ketika ternyata salah.

Tanda kurung di sekitar predikat adalah pengingat penting bahwa dalam membutuhkan klausa, kebalikan dari predikat bukanlah kebalikan dari predikat itu sendiri. Begitu,

requires is_trivial_v<typename T::value_type> 

berarti sifat tersebut benar dan mengembalikan true. Di mana

!is_trivial_v<typename T::value_type> 

akan berarti "sifatnya benar dan mengembalikan salah"
Pembalikan logis sebenarnya dari predikat pertama BUKAN ("sifat itu benar dan mengembalikan benar") == "sifat itu TIDAK BENAR atau mengembalikan salah" - ini dicapai dengan cara yang sedikit lebih kompleks - melalui definisi eksplisit dari konsep:

template <typename T>
concept value_type_valid_and_trivial 
  = is_trivial_v<typename T::value_type>; 
 
template <typename T>
  requires (!value_type_valid_and_trivial<T>)
void fun(T v); 

Konjungsi dan Disjungsi


Operator konjungsi dan disjungsi logis terlihat seperti biasa, tetapi sebenarnya bekerja sedikit berbeda dari pada C ++ normal.

Pertimbangkan dua cuplikan kode yang sangat mirip.

Yang pertama adalah predikat tanpa tanda kurung:

template <typename T, typename U>
  requires std::is_trivial_v<typename T::value_type>
		|| std::is_trivial_v<typename U::value_type>
void fun(T v, U u); 

Yang kedua adalah dengan tanda kurung:

template <typename T, typename U>
  requires (std::is_trivial_v<typename T::value_type>
		 || std::is_trivial_v<typename U::value_type>)
void fun(T v, U u); 

Perbedaannya hanya pada tanda kurung. Tetapi karena ini, dalam templat kedua, tidak ada dua kendala yang disatukan oleh "keharusan-klausa", tetapi satu disatukan oleh OR logis atau biasa.

Perbedaan ini adalah sebagai berikut. Pertimbangkan kodenya

std::optional<int> oi {};
int i {};
fun(i, oi);

Di sini templat dipakai oleh tipe int dan std :: opsional.

Dalam kasus pertama, tipe int :: value_type tidak valid, dan batasan pertama karenanya tidak dipenuhi.

Tetapi tipe opsional :: value_type valid, sifat kedua mengembalikan true, dan karena ada operator ATAU di antara kendala, keseluruhan predikat puas secara keseluruhan.

Dalam kasus kedua, ini adalah ekspresi tunggal yang berisi jenis yang tidak valid, karena itu tidak valid secara umum dan predikat tidak puas. Jadi kurung sederhana secara tidak kasat mata mengubah makna dari apa yang terjadi.

Kesimpulannya


Tentu saja, tidak semua fitur konsep ditampilkan di sini. Saya hanya tidak melangkah lebih jauh. Tetapi sebagai kesan pertama - ide yang sangat menarik dan implementasi bingung yang agak aneh. Dan sintaks yang lucu dengan pengulangan membutuhkan, yang benar-benar membingungkan. Apakah benar ada begitu sedikit kata dalam bahasa Inggris sehingga Anda harus menggunakan satu kata untuk tujuan yang sama sekali berbeda?

Ide dengan kode yang dikompilasi pasti bagus. Bahkan agak mirip dengan "quasi-quoting" di makro sintaks. Tetapi apakah itu layak mencampur sintaks khusus untuk memeriksa tipe kembali? IMHO, untuk ini hanya perlu membuat kata kunci terpisah.

Pencampuran implisit dari konsep "benar / salah" dan "mengkompilasi / tidak mengkompilasi" dalam satu tumpukan, dan sebagai hasilnya, lelucon dengan tanda kurung juga salah. Ini adalah konsep yang berbeda, dan mereka harus ada secara ketat dalam konteks yang berbeda (meskipun saya mengerti dari mana asalnya - dari aturan SFINAE, di mana kode yang tidak dikompilasi hanya diam-diam mengeluarkan spesialisasi dari pertimbangan). Tetapi jika tujuan dari konsep ini adalah untuk membuat kode sejelas mungkin, apakah layak untuk menyeret semua hal tersirat ini ke fitur-fitur baru?

Artikel ini ditulis terutama berdasarkan
akrzemi1.wordpress.com/2020/01/29/requires-expression
akrzemi1.wordpress.com/2020/03/26/requires-clause
(ada lebih banyak contoh dan fitur menarik)
dengan tambahan saya dari sumber lain,
semua contoh dapat diperiksawandbox.org

All Articles