Chapter Operator Perbandingan memperkenalkan operator spaceship <=> beserta cara paling ringkas memakainya pada class: menandai definisi dengan = default, lalu compiler menyusun perbandingan member demi member sesuai urutan deklarasi. Ringkas memang, tetapi kaku — = default selalu membandingkan semua member dengan perbandingan alami masing-masing. Sering kali kebutuhan class berbeda: ada member yang sebaiknya tidak ikut, urutan yang ingin dibalik, atau member floating-point yang perbandingan alaminya tidak total. Pada Chapter ini kita menulis operator<=> kustom untuk ketiga kebutuhan tersebut, mengenal tiga kategori hasilnya, dan memverifikasi bahwa enam operator lain tetap tersedia — empat lewat substitusi <=>, dua lewat == yang menyertainya. Rujukan resmi pembahasan ini adalah halaman Default comparisons
dan std::strong_order
pada cppreference.
Kapan <=> Kustom Dibutuhkan #
Sebelum menulis kode, kita petakan dulu situasi yang membuat = default tidak mencukupi. Ketiganya berasal dari akar yang sama: perbandingan default mengikuti struktur class secara mentah, sedangkan makna urutan ditentukan domain masalah. Daftar berikut merangkum tiga kasus yang sering muncul.
- Member yang tidak ikut dibandingkan. Perbandingan default menyertakan seluruh member; member yang hanya identitas teknis — misalnya nomor rekening — sebaiknya tidak memengaruhi kesetaraan maupun urutan.
- Urutan perbandingan berbeda dari urutan deklarasi. Default menelusuri member dari atas ke bawah; bila kriteria utama dideklarasikan belakangan, hasil urutannya tidak sesuai kebutuhan.
- Member dengan tipe tanpa perbandingan total. Floating-point menghasilkan
std::partial_orderingkarena memiliki nilai tak terbanding seperti NaN; class untuk antrian atau kunci map butuh urutan total sehingga member tersebut diperlakukan khusus.
Menulis operator<=> sendiri mengubah pembagian kerja dengan compiler. Empat operator relasional <, <=, >, >= tetap muncul otomatis lewat substitusi hasil <=>. Dua operator kesetaraan berbeda nasibnya: begitu <=> ditulis sendiri, compiler tidak lagi menghasilkan == secara implisit, sehingga kita mendeklarasikannya sendiri agar konsisten dengan logika urutan yang ditulis.
Class Rekening dengan <=> Kustom #
Program berikut memodelkan antrian pemrosesan rekening: prioritas lebih tinggi diproses lebih dulu; bila prioritas sama, saldo menjadi penentu. Nomor rekening hanya identitas sehingga sengaja dikeluarkan dari perbandingan. Saldo bertipe double, dibungkus std::strong_order agar NaN tidak melumpuhkan urutan.
#include <compare>
#include <iostream>
#include <limits>
#include <string>
class Rekening {
public:
Rekening(std::string nomorAwal, int prioritasAwal, double saldoAwal)
: nomor(nomorAwal), prioritas(prioritasAwal), saldo(saldoAwal) {}
// <=> kustom: prioritas lebih dulu, lalu saldo; nomor tidak ikut dibandingkan.
std::strong_ordering operator<=>(const Rekening& lain) const {
if (auto cmp = prioritas <=> lain.prioritas; cmp != 0) {
return cmp;
}
return std::strong_order(saldo, lain.saldo);
}
// == mengikuti logika urutan yang sama; != otomatis muncul dari ==.
bool operator==(const Rekening& lain) const {
return (*this <=> lain) == 0;
}
const std::string& nomorRekening() const { return nomor; }
private:
std::string nomor; // identitas unik: sengaja tidak ikut dibandingkan
int prioritas; // dibandingkan PERTAMA (berbeda dari urutan deklarasi)
double saldo; // dibandingkan kedua: dibungkus std::strong_order
};
const char* namaUrutan(std::strong_ordering hasil) {
if (hasil < 0) return "less";
if (hasil > 0) return "greater";
return "equal";
}
int main() {
std::cout << std::boolalpha;
Rekening danaDarurat{"N-001", 2, 500.0};
Rekening tagihanListrik{"N-002", 1, 750.0};
Rekening danaCadangan{"N-003", 2, 500.0}; // urutan sama dengan danaDarurat
Rekening saldoTakTersedia{"N-004", 2, std::numeric_limits<double>::quiet_NaN()};
std::cout << "Enam operator dari satu logika urutan:\n";
std::cout << "danaDarurat < tagihanListrik : " << (danaDarurat < tagihanListrik) << "\n";
std::cout << "danaDarurat > tagihanListrik : " << (danaDarurat > tagihanListrik) << "\n";
std::cout << "danaDarurat <= tagihanListrik : " << (danaDarurat <= tagihanListrik) << "\n";
std::cout << "danaDarurat >= tagihanListrik : " << (danaDarurat >= tagihanListrik) << "\n";
std::cout << "danaDarurat == tagihanListrik : " << (danaDarurat == tagihanListrik) << "\n";
std::cout << "danaDarurat != tagihanListrik : " << (danaDarurat != tagihanListrik) << "\n\n";
std::cout << "Hasil <=> langsung:\n";
std::cout << "danaDarurat <=> tagihanListrik : " << namaUrutan(danaDarurat <=> tagihanListrik) << "\n";
std::cout << "tagihanListrik <=> danaDarurat : " << namaUrutan(tagihanListrik <=> danaDarurat) << "\n";
std::cout << "danaDarurat <=> danaCadangan : " << namaUrutan(danaDarurat <=> danaCadangan) << "\n";
std::cout << "danaDarurat == danaCadangan : " << (danaDarurat == danaCadangan) << "\n\n";
std::cout << "Saldo NaN tetap terurut lewat std::strong_order:\n";
std::cout << "saldoTakTersedia <=> danaDarurat : " << namaUrutan(saldoTakTersedia <=> danaDarurat) << "\n";
std::cout << "saldoTakTersedia > danaDarurat : " << (saldoTakTersedia > danaDarurat) << "\n";
std::cout << "saldoTakTersedia == saldoTakTersedia : " << (saldoTakTersedia == saldoTakTersedia) << "\n";
return 0;
}Output yang Dihasilkan:
Enam operator dari satu logika urutan:
danaDarurat < tagihanListrik : false
danaDarurat > tagihanListrik : true
danaDarurat <= tagihanListrik : false
danaDarurat >= tagihanListrik : true
danaDarurat == tagihanListrik : false
danaDarurat != tagihanListrik : true
Hasil <=> langsung:
danaDarurat <=> tagihanListrik : greater
tagihanListrik <=> danaDarurat : less
danaDarurat <=> danaCadangan : equal
danaDarurat == danaCadangan : true
Saldo NaN tetap terurut lewat std::strong_order:
saldoTakTersedia <=> danaDarurat : greater
saldoTakTersedia > danaDarurat : true
saldoTakTersedia == saldoTakTersedia : trueMari kita bedah bagian pentingnya:
- Baris 8-9 (constructor): constructor menyimpan
nomor,prioritas, lalusaldo— urutan inisialisasi mengikuti urutan deklarasi member agar tidak memicu peringatan-Wreorder. - Baris 12 (deklarasi
operator<=>): fungsi mengembalikanstd::strong_ordering, kategori urutan yang paling ketat, dan menerimaconst Rekening&tanpa mengubah objek (const). - Baris 13-15 (idiom inti):
ifdengan initializer (C++17) menyimpan hasilprioritas <=> lain.prioritaskecmp; bilacmp != 0, urutan sudah memutuskan dan hasilnya langsung dikembalikan. - Baris 16 (
std::strong_order(saldo, lain.saldo)):<=>alami pada double hanya menghasilkanstd::partial_ordering; fungsistd::strong_ordermenaikkannya menjadi urutan total bertipestd::strong_ordering. - Baris 20-22 (
operator==): kesetaraan ditulis eksplisit dan diturunkan dari<=>(hasilnya sama dengan0);operator!=tidak ditulis karena compiler menyusunnya sebagai kebalikan==. - Baris 27-29 (deklarasi member):
nomordideklarasikan paling awal tetapi tidak dibandingkan sama sekali, danprioritasyang dideklarasikan kedua justru dibandingkan pertama — urutan perbandingan kini ditentukan oleh kode, bukan oleh urutan deklarasi. - Baris 41-44 (empat objek):
saldoTakTersediamemakai saldostd::numeric_limits<double>::quiet_NaN(), model untuk saldo yang nilainya tidak berhasil diproses. - Baris 47-52 (enam operator): seluruh operator
<,>,<=,>=,==,!=sah dipakai — empat yang pertama lahir dari substitusi<=>, dua sisanya darioperator==yang kita tulis. - Baris 55-58 (hasil
<=>langsung):danaDarurat <=> danaCadanganmenghasilkanequalmeski nomornya berbeda, sehinggadanaDarurat == danaCadanganbernilaitrue— kesetaraan mengikuti logika urutan yang sama. - Baris 61-63 (saldo NaN):
saldoTakTersedia > danaDaruratbernilaitruekarena urutan total menempatkan NaN pada posisi paling akhir, dan perbandingan terakhir menunjukkan dua NaN berpola bit sama tetap memperoleh keputusanequallewatstd::strong_order.
Dari mana enam operator itu berasal? Compiler menafsirkan a < b sebagai (a <=> b) < 0, a > b sebagai (a <=> b) > 0, dan pola serupa untuk <= serta >= — mekanisme yang disebut rewritten candidates atau substitusi. != disusun compiler dari == yang kita tulis.
Tiga Kategori Hasil Perbandingan #
Tipe kembalian operator<=> menentukan makna kesetaraan yang dijanjikan class. C++20 menyediakan tiga kategori, dari yang paling ketat hingga paling longgar. Tabel berikut merangkum perbedaannya.
| Kategori | Dua nilai setara | Nilai tak terbanding | Sumber lazim |
|---|---|---|---|
std::strong_ordering | tidak dapat dibedakan (substitutable) | tidak diizinkan | tipe integer, pointer |
std::weak_ordering | masih dapat dibedakan | tidak diizinkan | perbandingan tanpa sensitivitas case |
std::partial_ordering | masih dapat dibedakan | diizinkan sebagai unordered | float dan double |
std::strong_ordering paling ketat: dua nilai yang equal boleh saling menggantikan. std::weak_ordering melonggarkannya — "BUDI" dan "Budi" setara pada perbandingan tanpa case, tetapi tetap dua string berbeda. std::partial_ordering paling longgar karena mengizinkan pasangan nilai tak terbanding; sumber utamanya floating-point — NaN tidak lebih besar, tidak lebih kecil, dan tidak sama dengan nilai mana pun, termasuk dirinya sendiri.
Urutan Total dengan std::strong_order #
Program kedua membedah ketiga kategori langsung pada tipe bawaan. Perbandingan <=> pada double menghasilkan std::partial_ordering; begitu salah satu operand NaN, hasilnya unordered. Fungsi std::strong_order dari header <compare> menyusun ulang pasangan floating-point mengikuti total order IEEE 754: -0.0 sebelum 0.0, NaN paling akhir. Setiap pasangan nilai kini memperoleh keputusan yang pasti.
#include <algorithm>
#include <cctype>
#include <compare>
#include <iostream>
#include <limits>
#include <string>
const char* namaUrutan(std::strong_ordering hasil) {
if (hasil < 0) return "less";
if (hasil > 0) return "greater";
return "equal";
}
const char* namaUrutan(std::weak_ordering hasil) {
if (hasil < 0) return "less";
if (hasil > 0) return "greater";
return "equivalent";
}
const char* namaUrutan(std::partial_ordering hasil) {
if (hasil == std::partial_ordering::unordered) return "unordered";
if (hasil < 0) return "less";
if (hasil > 0) return "greater";
return "equivalent";
}
// weak_ordering: "BUDI" setara dengan "Budi" meski dua string tersebut berbeda.
std::weak_ordering bandingkanTanpaCase(const std::string& kiri, const std::string& kanan) {
const size_t panjangSama = std::min(kiri.size(), kanan.size());
for (size_t i = 0; i < panjangSama; ++i) {
const int cKiri = std::toupper(static_cast<unsigned char>(kiri[i]));
const int cKanan = std::toupper(static_cast<unsigned char>(kanan[i]));
if (cKiri != cKanan) {
return cKiri < cKanan ? std::weak_ordering::less : std::weak_ordering::greater;
}
}
return kiri.size() <=> kanan.size();
}
int main() {
std::cout << std::boolalpha;
const double kecil = 500.0;
const double besar = 750.0;
const double takTersedia = std::numeric_limits<double>::quiet_NaN();
std::cout << "Kategori <=> pada tipe bawaan:\n";
std::cout << "500.0 <=> 750.0 : " << namaUrutan(kecil <=> besar) << "\n";
std::cout << "NaN <=> 500.0 : " << namaUrutan(takTersedia <=> kecil) << "\n";
std::cout << "-0.0 <=> 0.0 : " << namaUrutan(-0.0 <=> 0.0) << "\n\n";
std::cout << "Jebakan NaN: semua perbandingan menghasilkan false:\n";
std::cout << "(NaN <=> 500.0) < 0 : " << ((takTersedia <=> kecil) < 0) << "\n";
std::cout << "(NaN <=> 500.0) == 0 : " << ((takTersedia <=> kecil) == 0) << "\n";
std::cout << "(NaN <=> 500.0) > 0 : " << ((takTersedia <=> kecil) > 0) << "\n\n";
std::cout << "std::strong_order menyusun urutan total:\n";
std::cout << "strong_order(NaN, 500.0) : " << namaUrutan(std::strong_order(takTersedia, kecil)) << "\n";
std::cout << "strong_order(-0.0, 0.0) : " << namaUrutan(std::strong_order(-0.0, 0.0)) << "\n";
std::cout << "strong_order(NaN, NaN) : " << namaUrutan(std::strong_order(takTersedia, takTersedia)) << "\n\n";
std::cout << "weak_ordering: setara belum tentu identik:\n";
std::cout << "Budi vs BUDI : " << namaUrutan(bandingkanTanpaCase("Budi", "BUDI")) << "\n";
std::cout << "Budi vs Budiman : " << namaUrutan(bandingkanTanpaCase("Budi", "Budiman")) << "\n";
std::cout << "budi vs siti : " << namaUrutan(bandingkanTanpaCase("budi", "siti")) << "\n";
std::cout << "\"Budi\" == \"BUDI\" : " << (std::string("Budi") == std::string("BUDI")) << "\n";
return 0;
}Output yang Dihasilkan:
Kategori <=> pada tipe bawaan:
500.0 <=> 750.0 : less
NaN <=> 500.0 : unordered
-0.0 <=> 0.0 : equivalent
Jebakan NaN: semua perbandingan menghasilkan false:
(NaN <=> 500.0) < 0 : false
(NaN <=> 500.0) == 0 : false
(NaN <=> 500.0) > 0 : false
std::strong_order menyusun urutan total:
strong_order(NaN, 500.0) : greater
strong_order(-0.0, 0.0) : less
strong_order(NaN, NaN) : equal
weak_ordering: setara belum tentu identik:
Budi vs BUDI : equivalent
Budi vs Budiman : less
budi vs siti : less
"Budi" == "BUDI" : falsePoin-poin pentingnya:
- Baris 8-22 (tiga overload
namaUrutan): fungsi bernama sama menerima tiga kategori berbeda dan menerjemahkan hasil<=>menjadi kata yang terbaca; overload yang dipilih mengikuti tipe argumen. - Baris 19-21 (cabang
unordered): padastd::partial_ordering, nilaiunorderedharus diperiksa lebih dulu dan eksplisit, karena nilai tersebut gagal pada ketiga uji< 0,== 0, dan> 0. - Baris 25-38 (
bandingkanTanpaCase): fungsi membandingkan karakter demi karakter setelah dinaikkanstd::toupper, lalu menutup dengankiri.size() <=> kanan.size(); hasilstd::strong_orderingdari perbandingan panjang otomatis terkonversi kestd::weak_ordering. - Baris 48-50 (kategori tipe bawaan):
500.0 <=> 750.0menghasilkanlessdan-0.0 <=> 0.0menghasilkanequivalent, keduanya bertipestd::partial_orderingkarena operandnya double. - Baris 53-55 (jebakan NaN): ketiga uji terhadap
NaN <=> 500.0menghasilkanfalse—less,equal, maupungreatertidak memegang kebenaran, sehingga urutan gagal diputuskan. - Baris 58-60 (
std::strong_ordermemulihkan urutan): NaN ditempatkan setelah semua bilangan (greater),-0.0sebelum0.0(less), dan dua NaN berpola bit sama memperoleh keputusanequal. - Baris 63-66 (makna
equivalent):"Budi"dan"BUDI"setara (equivalent) pada perbandingan tanpa case, tetapi==pada string tetapfalse— setara belum tentu identik.
Idiom Penulisan dan Jebakan yang Sering Terjadi #
Pola pada class Rekening patut dijadikan idiom baku: bandingkan member satu per satu dengan <=>, dan segera kembalikan hasilnya begitu urutan terputus. Member dengan perbandingan tidak teratur — seperti double — dibungkus std::strong_order agar fungsi tetap menjanjikan std::strong_ordering. Rantai if (auto cmp = ...; cmp != 0) return cmp; membuat prioritas terbaca jelas.
Jebakan paling umum: lupa memakai hasil std::strong_order untuk member tanpa perbandingan total. Misalkan fungsi berikut menjanjikan std::strong_ordering, tetapi mengembalikan <=> alami member double:
class Suhu {
public:
std::strong_ordering operator<=>(const Suhu& lain) const {
return celsius <=> lain.celsius; // partial_ordering, bukan strong_ordering
}
private:
double celsius = 0.0;
};Kode tersebut ditolak compiler; bagian inti pesan galatnya:
error: could not convert '(celsius <=> lain.celsius)' from 'std::partial_ordering' to 'std::strong_ordering'Perbaikan: bungkus member dengan std::strong_order dan pakai hasilnya sebagai nilai kembalian:
std::strong_ordering operator<=>(const Suhu& lain) const {
if (auto cmp = std::strong_order(celsius, lain.celsius); cmp != 0) {
return cmp;
}
return std::strong_ordering::equal;
}Kini kita menguasai perbandingan dari dua sisi: nilai bool dari enam operator perbandingan, dan urutan penuh dari satu definisi <=>. Keduanya bertemu pada kebutuhan berikutnya — menggabungkan beberapa keputusan menjadi kondisi yang lebih kompleks. Di Chapter berikutnya kita menyusun gabungan itu dengan operator logika &&, ||, dan ! beserta aturan short-circuit evaluation.