<=>
. 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.