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_ordering karena 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.

cpp
#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:

text
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 : true

Mari kita bedah bagian pentingnya:

  1. Baris 8-9 (constructor): constructor menyimpan nomor, prioritas, lalu saldo — urutan inisialisasi mengikuti urutan deklarasi member agar tidak memicu peringatan -Wreorder.
  2. Baris 12 (deklarasi operator<=>): fungsi mengembalikan std::strong_ordering, kategori urutan yang paling ketat, dan menerima const Rekening& tanpa mengubah objek (const).
  3. Baris 13-15 (idiom inti): if dengan initializer (C++17) menyimpan hasil prioritas <=> lain.prioritas ke cmp; bila cmp != 0, urutan sudah memutuskan dan hasilnya langsung dikembalikan.
  4. Baris 16 (std::strong_order(saldo, lain.saldo)): <=> alami pada double hanya menghasilkan std::partial_ordering; fungsi std::strong_order menaikkannya menjadi urutan total bertipe std::strong_ordering.
  5. Baris 20-22 (operator==): kesetaraan ditulis eksplisit dan diturunkan dari <=> (hasilnya sama dengan 0); operator!= tidak ditulis karena compiler menyusunnya sebagai kebalikan ==.
  6. Baris 27-29 (deklarasi member): nomor dideklarasikan paling awal tetapi tidak dibandingkan sama sekali, dan prioritas yang dideklarasikan kedua justru dibandingkan pertama — urutan perbandingan kini ditentukan oleh kode, bukan oleh urutan deklarasi.
  7. Baris 41-44 (empat objek): saldoTakTersedia memakai saldo std::numeric_limits<double>::quiet_NaN(), model untuk saldo yang nilainya tidak berhasil diproses.
  8. Baris 47-52 (enam operator): seluruh operator <, >, <=, >=, ==, != sah dipakai — empat yang pertama lahir dari substitusi <=>, dua sisanya dari operator== yang kita tulis.
  9. Baris 55-58 (hasil <=> langsung): danaDarurat <=> danaCadangan menghasilkan equal meski nomornya berbeda, sehingga danaDarurat == danaCadangan bernilai true — kesetaraan mengikuti logika urutan yang sama.
  10. Baris 61-63 (saldo NaN): saldoTakTersedia > danaDarurat bernilai true karena urutan total menempatkan NaN pada posisi paling akhir, dan perbandingan terakhir menunjukkan dua NaN berpola bit sama tetap memperoleh keputusan equal lewat std::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.

KategoriDua nilai setaraNilai tak terbandingSumber lazim
std::strong_orderingtidak dapat dibedakan (substitutable)tidak diizinkantipe integer, pointer
std::weak_orderingmasih dapat dibedakantidak diizinkanperbandingan tanpa sensitivitas case
std::partial_orderingmasih dapat dibedakandiizinkan sebagai unorderedfloat 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.

cpp
#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:

text
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"  : false

Poin-poin pentingnya:

  1. 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.
  2. Baris 19-21 (cabang unordered): pada std::partial_ordering, nilai unordered harus diperiksa lebih dulu dan eksplisit, karena nilai tersebut gagal pada ketiga uji < 0, == 0, dan > 0.
  3. Baris 25-38 (bandingkanTanpaCase): fungsi membandingkan karakter demi karakter setelah dinaikkan std::toupper, lalu menutup dengan kiri.size() <=> kanan.size(); hasil std::strong_ordering dari perbandingan panjang otomatis terkonversi ke std::weak_ordering.
  4. Baris 48-50 (kategori tipe bawaan): 500.0 <=> 750.0 menghasilkan less dan -0.0 <=> 0.0 menghasilkan equivalent, keduanya bertipe std::partial_ordering karena operandnya double.
  5. Baris 53-55 (jebakan NaN): ketiga uji terhadap NaN <=> 500.0 menghasilkan false — less, equal, maupun greater tidak memegang kebenaran, sehingga urutan gagal diputuskan.
  6. Baris 58-60 (std::strong_order memulihkan urutan): NaN ditempatkan setelah semua bilangan (greater), -0.0 sebelum 0.0 (less), dan dua NaN berpola bit sama memperoleh keputusan equal.
  7. Baris 63-66 (makna equivalent): "Budi" dan "BUDI" setara (equivalent) pada perbandingan tanpa case, tetapi == pada string tetap false — 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:

cpp Tidak kompilasi: partial_ordering tidak bisa dikonversi ke strong_ordering
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:

text
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:

cpp Perbaikan: bungkus member partial dengan std::strong_order
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.