. Belum lama berselang, Simon Brand menerbitkan sebuah posting yang berisi...">

Operator pesawat ruang angkasa baru di C ++ 20

C ++ 20 menambahkan operator baru yang disebut "pesawat ruang angkasa": <=> . Belum lama berselang, Simon Brand menerbitkan sebuah posting yang berisi informasi konseptual terperinci tentang apa operator ini dan untuk tujuan apa ia digunakan. Tugas utama tulisan ini adalah mempelajari aplikasi spesifik operator baru "aneh" dan operator== analognya operator== , serta merumuskan beberapa rekomendasi untuk penggunaannya dalam pengkodean sehari-hari.


Perbandingan


Sudah lazim 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 penuh perhatian akan melihat bahwa ini sebenarnya lebih sedikit verbose daripada seharusnya dalam kode sebelum C ++ 20. Lebih lanjut tentang ini nanti.

Anda perlu menulis banyak kode standar untuk memastikan bahwa jenis kami dapat dibandingkan dengan jenis yang sama. Oke, kita akan mencari tahu sebentar. Kemudian datang seseorang yang menulis seperti 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 tidak akan dikompilasi.

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

Masalahnya adalah bahwa constexpr dilupakan dalam fungsi perbandingan. Kemudian beberapa akan menambahkan constexpr ke semua operator perbandingan. Beberapa hari kemudian, seseorang akan menambahkan is_gt , tetapi perhatikan bahwa semua operator pembanding tidak memiliki spesifikasi pengecualian, dan Anda harus melalui proses yang sama yaitu menambahkan noexcept untuk masing-masing dari 5 kelebihan beban.

Di sinilah operator pesawat ruang angkasa C ++ 20 yang baru datang untuk membantu kami. Mari kita lihat bagaimana Anda dapat menulis IntWrapper asli di 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 untuk operator pesawat ruang angkasa, sehingga ia mengembalikan jenis yang cocok untuk fungsi default kami. Dalam cuplikan di atas, tipe pengembalian auto akan menjadi std::strong_ordering .

Kami tidak hanya menghapus 5 baris tambahan, tetapi kami bahkan tidak perlu menentukan apa pun, kompiler akan melakukannya untuk kami. is_lt tetap tidak berubah dan hanya berfungsi, sementara tetap constexpr , meskipun kami tidak secara eksplisit menentukan ini di operator<=> default kami operator<=> . Ini bagus, tetapi beberapa orang mungkin bingung mengapa is_lt diizinkan untuk dikompilasi bahkan jika itu tidak menggunakan operator pesawat ruang angkasa sama sekali. Mari kita temukan jawaban untuk pertanyaan ini.

Menulis Ulang Ekspresi


Dalam C ++ 20, kompiler diperkenalkan ke dalam konsep baru terkait dengan ekspresi "ditulis ulang". Operator pesawat ruang angkasa, bersama dengan operator== , adalah salah satu dari dua kandidat pertama yang dapat ditulis ulang. Untuk contoh yang lebih spesifik dari penulisan ulang ekspresi, mari kita lihat contoh yang diberikan di is_lt .

Saat menyelesaikan kelebihan beban, kompiler akan memilih dari sekumpulan kandidat yang paling cocok, yang masing-masing sesuai dengan operator yang kita butuhkan. Proses pemilihan berubah sangat sedikit untuk operasi perbandingan dan operasi ekivalensi, ketika kompiler juga harus mengumpulkan kandidat transkrip dan disintesis khusus ( [over.match.oper] /3.4 ).

Untuk ekspresi kita a < b standar menyatakan bahwa kita dapat mencari tipe a untuk operator<=> atau fungsi operator<=> yang menerima jenis ini. Inilah yang dilakukan oleh kompiler dan menemukan bahwa tipe a sebenarnya berisi IntWrapper::operator<=> . Kompiler kemudian diizinkan untuk menggunakan operator ini dan menulis ulang ekspresi a < b as (a <=> b) < 0 . Ungkapan ditulis ulang ini kemudian digunakan sebagai kandidat untuk resolusi overload normal.

Anda mungkin bertanya mengapa ungkapan yang ditulis ulang ini benar. Ketepatan ekspresi sebenarnya mengikuti dari semantik yang disediakan oleh operator pesawat ruang angkasa. <=> adalah perbandingan tiga arah, yang menyiratkan bahwa Anda tidak hanya mendapatkan hasil biner, tetapi juga pesanan (dalam kebanyakan kasus). Jika Anda memiliki pesanan, Anda dapat mengungkapkan pesanan ini dalam hal operasi perbandingan apa pun. Contoh cepat, ekspresi 4 <=> 5 dalam C ++ 20 akan mengembalikan hasil std::strong_ordering::less . Hasil dari std::strong_ordering::less menyiratkan bahwa 4 tidak hanya berbeda dari 5 tetapi juga sangat kurang dari nilai ini, yang membuat aplikasi operasi (4 <=> 5) < 0 benar dan akurat untuk menggambarkan hasil kami.

Dengan menggunakan informasi di atas, kompiler dapat menggunakan operator pembanding umum (mis., < , > , Dll.) Dan menulis ulang dalam hal operator ruang angkasa. Dalam standar, ekspresi ditulis ulang sering disebut sebagai (a <=> b) @ 0 mana @ mewakili operasi perbandingan.

Menyintesis Ekspresi


Pembaca mungkin telah memperhatikan referensi halus untuk ekspresi "disintesis" di atas, dan mereka juga memainkan peran dalam proses penulisan ulang pernyataan ini. Pertimbangkan fungsi berikut:

 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 sebelum C ++ 20, dan cara untuk menyelesaikan masalah ini adalah menambahkan beberapa fungsi friend tambahan ke IntWrapper yang menempati sisi kiri int . Jika Anda mencoba membuat contoh ini menggunakan kompiler dan IntWrapper C ++ 20, Anda mungkin memperhatikan bahwa, sekali lagi, itu hanya berfungsi. Mari kita lihat mengapa kode di atas masih dikompilasi di C ++ 20.

Saat mengatasi kelebihan beban, kompiler juga akan mengumpulkan apa yang standar sebut kandidat "disintesis", atau ekspresi ditulis ulang dengan urutan parameter terbalik. Dalam contoh di atas, kompiler akan mencoba menggunakan ekspresi yang ditulis ulang (42 <=> a) < 0 , tetapi akan menemukan bahwa tidak ada konversi dari IntWrapper ke int untuk memenuhi sisi kiri, sehingga ekspresi yang ditulis ulang dibuang. Kompilator juga memanggil ekspresi “disintesis” 0 < (a <=> 42) dan mendeteksi bahwa konversi dari int ke IntWrapper melalui konstruktor konversi, sehingga kandidat ini digunakan.

Tujuan dari ekspresi yang disintesis adalah untuk menghindari kebingungan penulisan templat 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 kompiler tidak berhenti di masing-masing anggota kelas, tetapi 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 dalam daftar sub-objek mereka dan membandingkannya secara rekursif. Tentu saja, jika Anda ingin menulis sendiri fungsi-fungsi ini, Anda masih akan mendapat manfaat dari menulis ulang ekspresi oleh kompiler.

Tampak seperti bebek, berenang seperti bebek, dan dukun seperti operator==


Beberapa orang yang sangat pintar dalam komite standardisasi telah memperhatikan bahwa operator pesawat ruang angkasa akan selalu melakukan perbandingan unsur-unsur leksikografis, apa pun yang terjadi. Eksekusi tanpa syarat dari perbandingan leksikografis dapat menyebabkan kode yang tidak efisien, khususnya, dengan operator kesetaraan.

Contoh kanonik membandingkan dua garis. Jika Anda memiliki string "foobar" dan Anda membandingkannya dengan string "foo" menggunakan ==, Anda dapat mengharapkan operasi ini hampir konstan. Algoritma perbandingan string yang efektif adalah sebagai berikut:

  • Pertama, bandingkan ukuran kedua garis tersebut. Jika ukurannya berbeda, maka kembalikan false
  • Kalau tidak, melangkahlah melalui setiap elemen dari dua garis selangkah demi selangkah dan membandingkannya sampai ada perbedaan atau semua elemen berakhir. Kembalikan hasilnya.

Sesuai dengan aturan operator pesawat ruang angkasa, kita harus mulai dengan membandingkan setiap elemen hingga kita menemukan yang berbeda. Dalam contoh kami, "foobar" dan "foo" hanya ketika membandingkan 'b' dan '\0' akhirnya Anda mengembalikan false .

Untuk mengatasi ini, ada artikel P1185R2 , yang merinci bagaimana kompiler 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; }; 

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

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

Kode lama tidak akan rusak


Pada titik ini, Anda mungkin berpikir: baik, jika kompiler diizinkan untuk melakukan operasi penulisan ulang operator ini, apa yang akan terjadi jika saya mencoba mengecoh 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 bukan masalah besar. Model resolusi kelebihan dalam C ++ adalah arena di mana semua kandidat berbenturan. Dalam pertempuran khusus ini, kami memiliki tiga dari mereka:

  • 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 mengadopsi aturan resolusi kelebihan di C ++ 17, hasil dari panggilan ini akan dicampur, tetapi aturan resolusi kelebihan C ++ 20 diubah sehingga kompilator dapat menyelesaikan situasi ini ke beban berlebih yang paling logis.

Ada fase resolusi kelebihan ketika kompiler harus menyelesaikan serangkaian lintasan tambahan. Dalam C ++ 20, sebuah mekanisme baru telah muncul, di mana preferensi diberikan kepada overload yang tidak ditimpa atau disintesis, yang membuat IntWrapper::operator< kami IntWrapper::operator< membebani kandidat terbaik dan menyelesaikan ambiguitas. Mekanisme yang sama mencegah penggunaan kandidat yang disintesis alih-alih ekspresi yang ditulis ulang.

Pikiran terakhir


Operator pesawat ruang angkasa adalah tambahan selamat datang untuk C ++, karena dapat membantu menyederhanakan kode Anda dan menulis lebih sedikit, dan kadang-kadang lebih sedikit lebih baik. Jadi, kencangkan sabuk pengaman C ++ 20 Anda!

Kami mendesak 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 dilakukan pada 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 saat C ++ 20 selesai.

Seperti biasa, kami menunggu tanggapan Anda. Jangan ragu untuk mengirim komentar melalui email ke visualcpp@microsoft.com , melalui Twitter @visualc , atau Facebook Microsoft Visual Cpp .

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

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


All Articles