. Ada posting sementara kembali oleh Simon Merek kami sendiri...">

Sederhanakan Kode Anda Dengan Ilmu Roket: Operator Pesawat Luar Angkasa C ++ 20

C ++ 20 menambahkan operator baru, dijuluki sayang "operator pesawat ruang angkasa": <=> . Ada posting sementara kembali oleh Simon Merek kami sendiri merinci beberapa informasi mengenai operator baru ini bersama dengan beberapa informasi konseptual tentang apa itu dan apa. Tujuan dari posting ini adalah untuk mengeksplorasi beberapa aplikasi konkret dari operator baru yang aneh ini dan rekanan yang terkait, operator== (ya sudah diubah, menjadi lebih baik!), Semua sambil memberikan beberapa pedoman untuk penggunaannya dalam kode sehari-hari.



Perbandingan


Bukan hal yang aneh untuk melihat kode seperti berikut:

 struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  bool operator==(const IntWrapper& rhs) const { return value == rhs.value; }  bool operator!=(const IntWrapper& rhs) const { return !(*this == rhs);    }  bool operator<(const IntWrapper& rhs)  const { return value < rhs.value;  }  bool operator<=(const IntWrapper& rhs) const { return !(rhs < *this);    }  bool operator>(const IntWrapper& rhs)  const { return rhs < *this;        }  bool operator>=(const IntWrapper& rhs) const { return !(*this < rhs);    } }; 

Catatan: pembaca yang bermata elang akan melihat bahwa ini sebenarnya lebih sedikit verbose daripada yang seharusnya dalam kode pra-C ++ 20 karena fungsi-fungsi ini sebenarnya semua adalah teman yang bukan anggota, lebih banyak tentang itu nanti.

Itu banyak kode boilerplate untuk ditulis hanya untuk memastikan bahwa tipe saya sebanding dengan sesuatu dari tipe yang sama. Baiklah, kita berurusan sebentar. Kemudian datang seseorang yang menulis ini:

 constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) {  return a < b; } int main() {  static_assert(is_lt(0, 1)); } 

Hal pertama yang Anda perhatikan adalah bahwa program ini tidak dapat dikompilasi.

error C3615: constexpr function 'is_lt' cannot result in a constant expression

Ah! Masalahnya adalah kita lupa constexpr pada fungsi perbandingan kita, teman! Jadi kita pergi dan menambahkan constexpr ke semua operator perbandingan. Beberapa hari kemudian seseorang pergi dan menambahkan penolong is_gt tetapi pemberitahuan semua operator pembanding tidak memiliki spesifikasi pengecualian dan melalui proses yang sama untuk menambahkan noexcept untuk masing-masing dari 5 kelebihan beban.

Di sinilah operator ruang angkasa baru C ++ 20 melangkah untuk membantu kami. Mari kita lihat bagaimana IntWrapper asli dapat ditulis dalam dunia C ++ 20:

 #include <compare> struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  auto operator<=>(const IntWrapper&) const = default; }; 

Perbedaan pertama yang mungkin Anda perhatikan adalah penyertaan baru <compare> . Header <compare> bertanggung jawab untuk mengisi kompilator dengan semua jenis kategori perbandingan yang diperlukan bagi operator pesawat ruang angkasa untuk mengembalikan tipe yang sesuai dengan fungsi default kami. Dalam cuplikan di atas, tipe pengembalian auto akan disimpulkan menjadi std::strong_ordering .

Kita tidak hanya menghapus 5 baris berlebihan, tetapi kita bahkan tidak perlu mendefinisikan apa pun, kompilator melakukannya untuk kita! is_lt kami tetap tidak berubah dan hanya berfungsi sementara masih menjadi constexpr meskipun kami tidak secara eksplisit menentukannya di operator<=> default kami operator<=> . Itu bagus dan bagus, tetapi beberapa orang mungkin menggaruk-garuk kepala mengapa is_lt diizinkan untuk tetap mengkompilasi meskipun tidak menggunakan operator pesawat ruang angkasa sama sekali. Mari kita jelajahi jawaban untuk pertanyaan ini.

Menulis ulang ungkapan


Dalam C ++ 20, kompiler diperkenalkan dengan konsep baru yang disebut ekspresi "ditulis ulang". Operator pesawat ruang angkasa, bersama dengan operator== , adalah di antara dua kandidat pertama yang dikenakan ekspresi ulang. Untuk contoh yang lebih konkret dari penulisan ulang ekspresi, mari kita is_lt contoh yang disediakan di is_lt .

Selama resolusi kelebihan beban, kompiler akan memilih dari sekumpulan kandidat yang layak, yang semuanya cocok dengan operator yang kami cari. Proses pengumpulan kandidat diubah sedikit untuk kasus operasi relasional dan kesetaraan di mana kompiler juga harus mengumpulkan kandidat yang ditulis ulang dan disintesis khusus ( [over.match.oper] /3.4 ).

Untuk ungkapan kami a < b , standar menyatakan bahwa kita dapat mencari tipe operator<=> atau operator<=> fungsi lingkup namespace operator<=> yang menerima tipenya. Jadi kompiler melakukan dan menemukan bahwa, pada kenyataannya, tipe a memang IntWrapper::operator<=> . Kompiler kemudian diizinkan untuk menggunakan operator itu dan menulis ulang ekspresi a < b as (a <=> b) < 0 . Ekspresi yang ditulis ulang itu kemudian digunakan sebagai kandidat untuk resolusi overload normal.

Anda mungkin bertanya pada diri sendiri mengapa ungkapan yang ditulis ulang ini valid dan benar. Ketepatan ekspresi sebenarnya berasal dari semantik yang disediakan oleh operator pesawat ruang angkasa. <=> adalah perbandingan tiga arah yang menyiratkan bahwa Anda tidak hanya mendapatkan hasil biner, tetapi juga pemesanan (dalam kebanyakan kasus) dan jika Anda memiliki pemesanan, Anda dapat menyatakan pemesanan itu dalam hal operasi relasional apa pun. Contoh singkat, ekspresi 4 <=> 5 dalam C ++ 20 akan mengembalikan hasil Anda std::strong_ordering::less . Hasil std::strong_ordering::less menyiratkan bahwa 4 tidak hanya berbeda dari 5 tetapi juga sangat kurang dari nilai itu, ini membuat penerapan operasi (4 <=> 5) < 0 benar dan tepat akurat untuk menggambarkan hasil kami.

Menggunakan informasi di atas kompiler dapat mengambil operator relasional umum (yaitu < , > , dll.) Dan menulis ulang dalam hal operator ruang angkasa. Dalam standar, ungkapan yang ditulis ulang sering disebut sebagai (a <=> b) @ 0 mana @ mewakili operasi relasional apa pun.

Mensintesis ekspresi


Pembaca mungkin telah memperhatikan penyebutan halus ekspresi "disintesis" di atas dan mereka juga berperan dalam proses penulisan ulang operator ini. Pertimbangkan fungsi predikat yang berbeda:

 constexpr bool is_gt_42(const IntWrapper& a) {  return 42 < a; } 

Jika kami menggunakan definisi asli kami untuk IntWrapper kode ini tidak akan dikompilasi.

error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)

Ini masuk akal di lahan pra-C ++ 20, dan cara untuk mengatasi masalah ini adalah dengan menambahkan beberapa fungsi friend tambahan ke IntWrapper yang mengambil sisi kiri int . Jika Anda mencoba membuat sampel dengan kompiler C ++ 20 dan definisi IntWrapper C ++ 20 Anda, Anda mungkin memperhatikan bahwa itu, sekali lagi, “hanya berfungsi” —penggaruk kepala lainnya. Mari kita periksa mengapa kode di atas masih diizinkan untuk dikompilasi di C ++ 20.

Selama resolusi kelebihan beban, kompiler juga akan mengumpulkan apa yang disebut standar sebagai "disintesis" kandidat, atau ekspresi ditulis ulang dengan urutan parameter dibalik. Pada contoh di atas kompiler akan mencoba menggunakan ekspresi yang ditulis ulang (42 <=> a) < 0 tetapi ia akan menemukan bahwa tidak ada konversi dari IntWrapper ke int untuk memuaskan sisi kiri sehingga ekspresi yang ditulis ulang dibuang. Kompiler juga memunculkan ekspresi “disintesis” 0 < (a <=> 42) dan menemukan bahwa ada konversi dari int ke IntWrapper melalui konstruktor IntWrapper sehingga kandidat ini digunakan.

Tujuan dari ekspresi yang disintesis adalah untuk menghindari kekacauan dari kebutuhan untuk menulis lapisan fungsi friend untuk mengisi celah di mana objek Anda dapat dikonversi dari tipe lain. Ekspresi yang disintesis digeneralisasi ke 0 @ (b <=> a) .

Jenis yang lebih kompleks


Operator pesawat ruang angkasa yang dihasilkan oleh compiler tidak berhenti di satu anggota kelas, ia akan menghasilkan kumpulan perbandingan yang benar untuk semua sub-objek dalam tipe Anda:

 struct Basics {  int i;  char c;  float f;  double d;  auto operator<=>(const Basics&) const = default; }; struct Arrays {  int ai[1];  char ac[2];  float af[3];  double ad[2][2];  auto operator<=>(const Arrays&) const = default; }; struct Bases : Basics, Arrays {  auto operator<=>(const Bases&) const = default; }; int main() {  constexpr Bases a = { { 0, 'c', 1.f, 1. },                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };  constexpr Bases b = { { 0, 'c', 1.f, 1. },                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };  static_assert(a == b);  static_assert(!(a != b));  static_assert(!(a < b));  static_assert(a <= b);  static_assert(!(a > b));  static_assert(a >= b); } 

Kompiler tahu bagaimana memperluas anggota kelas yang array ke daftar sub-objek dan membandingkannya secara rekursif. Tentu saja, jika Anda ingin menulis sendiri fungsi-fungsi ini, Anda masih mendapatkan manfaat dari ekspresi penulisan ulang kompiler untuk Anda.

Terlihat Seperti Bebek, Berenang Seperti Bebek, dan Operator Dukun Suka operator==


Beberapa orang yang sangat pintar dalam komite standardisasi memperhatikan bahwa operator pesawat ruang angkasa akan selalu melakukan perbandingan leksikografis elemen apa pun yang terjadi. Perbandingan leksikografis tanpa syarat dapat menyebabkan kode yang dihasilkan tidak efisien dengan operator kesetaraan pada khususnya.

Contoh kanonik membandingkan dua string. Jika Anda memiliki string "foobar" dan Anda membandingkannya dengan string "foo" menggunakan == orang akan mengharapkan operasi yang hampir konstan. Algoritma perbandingan string yang efisien adalah sebagai berikut:

  • Pertama membandingkan ukuran dari dua string, jika ukurannya berbeda false , sebaliknya
  • melangkah melalui setiap elemen dari dua string secara bersamaan dan membandingkan sampai satu berbeda atau akhir tercapai, kembalikan hasilnya.

Di bawah aturan operator pesawat ruang angkasa kita harus mulai dengan perbandingan mendalam pada setiap elemen terlebih dahulu sampai kita menemukan yang berbeda. Dalam contoh kita tentang "foobar" dan "foo" hanya ketika membandingkan 'b' ke '\0' akhirnya Anda mengembalikan false .

Untuk mengatasi ini ada makalah, P1185R2 yang merinci cara bagi kompiler untuk menulis ulang dan menghasilkan operator== secara independen dari operator pesawat ruang angkasa. IntWrapper kami dapat ditulis sebagai berikut:

 #include <compare> struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  auto operator<=>(const IntWrapper&) const = default;  bool operator==(const IntWrapper&) const = default; }; 

Hanya satu langkah lagi ... namun, ada kabar baik; Anda sebenarnya tidak perlu menulis kode di atas, karena cukup menulis auto operator<=>(const IntWrapper&) const = default sudah cukup bagi kompiler untuk secara implisit menghasilkan operator== terpisah - dan lebih efisien - operator== untuk Anda!

Kompiler menerapkan aturan "penulisan ulang" yang sedikit diubah khusus untuk == dan != Di mana dalam operator ini ditulis ulang dalam hal operator== dan bukan operator<=> . Ini artinya != Juga mendapat manfaat dari pengoptimalan.

Kode Lama Tidak Akan Hancurkan


Pada titik ini Anda mungkin berpikir, OK jika kompiler diizinkan untuk melakukan bisnis penulisan ulang operator ini apa yang terjadi ketika saya mencoba mengakali kompiler:

 struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  auto operator<=>(const IntWrapper&) const = default;  bool operator<(const IntWrapper& rhs) const { return value < rhs.value; } }; constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) {  return a < b; } 

Jawabannya di sini adalah, Anda tidak. Model resolusi kelebihan di C ++ memiliki arena ini di mana semua kandidat melakukan pertempuran, dan dalam pertempuran khusus ini kami memiliki 3 kandidat:

  • IntWrapper::operator<(const IntWrapper& a, const IntWrapper& b)
  • IntWrapper::operator<=>(const IntWrapper& a, const IntWrapper& b)

(ditulis ulang)

  • IntWrapper::operator<=>(const IntWrapper& b, const IntWrapper& a)

(disintesis)

Jika kami menerima aturan resolusi kelebihan di C ++ 17 hasil panggilan itu akan menjadi ambigu, tetapi aturan resolusi kelebihan C ++ 20 diubah untuk memungkinkan kompilator menyelesaikan situasi ini ke beban berlebih yang paling logis.

Ada fase resolusi kelebihan dimana compiler harus melakukan serangkaian tiebreak. Dalam C ++ 20, ada tiebreak baru yang menyatakan kita harus lebih suka overload yang tidak ditulis ulang atau disintesis, ini membuat IntWrapper::operator< kelebihan kami IntWrapper::operator< kandidat terbaik dan menyelesaikan ambiguitas. Mesin yang sama ini mencegah kandidat yang disintesis untuk menginjak ekspresi yang ditulis ulang secara teratur.

Pikiran penutup


Operator pesawat ruang angkasa adalah tambahan yang disambut baik untuk C ++ dan itu adalah salah satu fitur yang akan menyederhanakan dan membantu Anda untuk menulis lebih sedikit kode, dan, kadang-kadang, lebih sedikit lebih banyak. Jadi kencangkan dengan operator pesawat ruang angkasa C ++ 20!

Kami mendorong Anda untuk keluar dan mencoba operator pesawat ruang angkasa, ini tersedia sekarang di Visual Studio 2019 di bawah /std:c++latest ! Sebagai catatan, perubahan yang diperkenalkan melalui P1185R2 akan tersedia di Visual Studio 2019 versi 16.2. Harap diingat bahwa operator pesawat ruang angkasa adalah bagian dari C ++ 20 dan dapat mengalami beberapa perubahan hingga waktu penyelesaian C ++ 20 selesai.

Seperti biasa, kami menyambut umpan balik Anda. Jangan ragu untuk mengirim komentar melalui email di visualcpp@microsoft.com , melalui Twitter @visualc , atau Facebook di Microsoft Visual Cpp . Juga, silakan ikuti saya di Twitter @starfreakclone .

Jika Anda mengalami masalah lain dengan MSVC di VS 2019, beri tahu kami melalui opsi Laporkan Masalah , baik dari penginstal atau Visual Studio IDE itu sendiri. Untuk saran atau laporan bug, beri tahu kami melalui DevComm.

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


All Articles