Topik ini membahas overloading operator aritmetika dan perbandingan pada tipe kelas, dengan kelas Pecahan sebagai contohnya. Operator +, ==, dan < ditulis dengan C++17, sedangkan bagian akhir menunjukkan cara C++20 memangkas penulisan perbandingan lewat spaceship operator <=>. Setelah topik ini, keenam operator perbandingan dapat ditulis lengkap dan saling konsisten pada tipe buatan sendiri.
Operator aritmetika menghasilkan nilai baru#
Operator aritmetika seperti + ditulis sebagai free function, menerima kedua operand, dan mengembalikan objek baru tanpa mengubah keduanya. Untuk Pecahan, hasil penjumlahan dihitung dengan menyamakan penyebut terlebih dahulu. Perbandingan pecahan juga tidak cukup membandingkan pembilangnya saja: a/b lebih kecil dari c/d apabila , sehingga pengujiannya memakai perkalian silang.
Pengisian majemuk (compound assignment) seperti += berbeda arahnya, yaitu mengubah objek kirinya, sehingga lazim ditulis sebagai member function. Satu kebiasaan yang menjaga konsistensi adalah menulis + dengan memanfaatkan += pada salinan operand kiri, sehingga kedua operator tidak pernah berbeda hasil. Topik ini menuliskan operator+ secara terpisah tanpa memanfaatkan += agar fokusnya tetap pada pola free function yang mengembalikan nilai baru.
Program berikut menuliskan operator+ beserta fondasi perbandingan, yaitu == dan <. Empat operator lainnya diturunkan dari keduanya.
#include <iostream>
class Pecahan {
public:
Pecahan(int pembilang, int penyebut) : pembilang(pembilang), penyebut(penyebut) {}
int getPembilang() const { return pembilang; }
int getPenyebut() const { return penyebut; }
private:
int pembilang;
int penyebut;
};
Pecahan operator+(const Pecahan& a, const Pecahan& b) {
return Pecahan(a.getPembilang() * b.getPenyebut() + b.getPembilang() * a.getPenyebut(),
a.getPenyebut() * b.getPenyebut());
}
// perbandingan memakai perkalian silang: a/b < c/d setara a*d < c*b
bool operator==(const Pecahan& a, const Pecahan& b) {
return a.getPembilang() * b.getPenyebut() == b.getPembilang() * a.getPenyebut();
}
bool operator<(const Pecahan& a, const Pecahan& b) {
return a.getPembilang() * b.getPenyebut() < b.getPembilang() * a.getPenyebut();
}
// empat operator lain diturunkan dari == dan <
bool operator!=(const Pecahan& a, const Pecahan& b) { return !(a == b); }
bool operator>(const Pecahan& a, const Pecahan& b) { return b < a; }
bool operator<=(const Pecahan& a, const Pecahan& b) { return !(b < a); }
bool operator>=(const Pecahan& a, const Pecahan& b) { return !(a < b); }
int main() {
Pecahan a(1, 2);
Pecahan b(1, 3);
Pecahan c = a + b;
std::cout << a.getPembilang() << "/" << a.getPenyebut() << " + "
<< b.getPembilang() << "/" << b.getPenyebut() << " = "
<< c.getPembilang() << "/" << c.getPenyebut() << "\n";
std::cout << std::boolalpha;
std::cout << "1/2 < 1/3 : " << (a < b) << "\n";
std::cout << "1/3 < 1/2 : " << (b < a) << "\n";
std::cout << "1/2 == 2/4 : " << (a == Pecahan(2, 4)) << "\n";
std::cout << "1/2 != 1/3 : " << (a != b) << "\n";
return 0;
}1/2 + 1/3 = 5/6
1/2 < 1/3 : false
1/3 < 1/2 : true
1/2 == 2/4 : true
1/2 != 1/3 : trueKeluaran mengonfirmasi bahwa 1/2 dan 2/4 dianggap sama meskipun representasinya berbeda, karena pengujiannya memakai perkalian silang. Baris perbandingan memerlukan tanda kurung, karena tanpanya std::cout << a < b terbaca sebagai (std::cout << a) < b dan gagal dikompilasi. Manipulator std::boolalpha mencetak nilai bool sebagai true dan false alih-alih 1 dan 0.
Operator perbandingan dari == dan <#
Pada C++17, == dan < adalah dua operator yang ditulis manual. Empat operator lainnya dapat diturunkan dari keduanya, yaitu != sebagai negasi ==, > dengan menukar operand pada <, serta <= dan >= sebagai negasi dari hasil penukaran tersebut. Pola turunan ini menjaga konsistensi, karena keenam operator berasal dari dua definisi yang sama dan tidak ada yang menulis logika perbandingannya dua kali.
Setiap definisi turunan cukup satu baris dan tidak memerlukan akses ke member private, karena hanya menyusun ulang pemanggilan operator fondasinya. Inilah alasan empat baris turunan pada program di atas ditulis rapat satu baris per operator. Ketika logika == atau < berubah, semua turunannya ikut berubah tanpa disunting satu per satu.
Spaceship operator dari C++20#
C++20 memperkenalkan operator perbandingan tiga arah (three-way comparison), yang ditulis <=> dan dikenal sebagai spaceship operator. Alih-alih mengembalikan bool, operator ini mengembalikan kategori urutan, misalnya std::strong_ordering yang nilainya less, equal, atau greater. Compiler menurunkan <, >, <=, dan >= dari hasil <=> secara otomatis, sehingga satu definisi menggantikan empat operator.
Operator == tetap ditulis terpisah, dan pada contoh berikut didefaultkan dengan = default sehingga compiler membandingkan member demi member. Perbandingan != ikut didapat tanpa kode tambahan karena compiler menyusunnya dari ==.
Perbandingan yang didefaultkan hanya benar karena constructor menyederhanakan pecahan ke bentuk kanonis, yaitu bentuk yang pembilang dan penyebutnya tidak lagi memiliki faktor persekutuan. Tanpa penyederhanaan, 2/4 dan 1/2 akan dianggap berbeda padahal nilainya sama. Adapun operator<=> tetap ditulis manual karena urutan member tidak sama dengan urutan nilai pecahan, sebagaimana 1/3 dan 1/2 yang pembilangnya sama tetapi 1/3 lebih kecil.
Karena fitur ini berasal dari C++20, kompilasinya memerlukan opsi -std=c++23 atau lebih baru pada compiler yang dipakai.
// tidak-dijalankan (butuh -std=c++23; perintah build ada di bawah)
#include <compare>
#include <iostream>
#include <numeric>
class Pecahan {
public:
Pecahan(int p, int q) : pembilang(p), penyebut(q) {
// bentuk kanonis: 2/4 disederhanakan menjadi 1/2, sehingga == bisa didefaultkan
int g = std::gcd(p, q);
if (g > 1) {
pembilang /= g;
penyebut /= g;
}
}
// satu operator <=> menggantikan <, >, <=, dan >=
std::strong_ordering operator<=>(const Pecahan& lain) const {
int kiri = pembilang * lain.penyebut;
int kanan = lain.pembilang * penyebut;
if (kiri < kanan) return std::strong_ordering::less;
if (kiri > kanan) return std::strong_ordering::greater;
return std::strong_ordering::equal;
}
// operator == dibuatkan otomatis oleh compiler (member demi member)
bool operator==(const Pecahan& lain) const = default;
int getPembilang() const { return pembilang; }
int getPenyebut() const { return penyebut; }
private:
int pembilang;
int penyebut;
};
int main() {
Pecahan a(1, 2);
Pecahan b(1, 3);
std::cout << std::boolalpha;
std::cout << "1/2 < 1/3 : " << (a < b) << "\n";
std::cout << "1/3 < 1/2 : " << (b < a) << "\n";
std::cout << "1/2 > 1/3 : " << (a > b) << "\n";
std::cout << "1/2 == 2/4 : " << (a == Pecahan(2, 4)) << "\n";
std::cout << "1/2 != 1/3 : " << (a != b) << "\n";
return 0;
}# kompilasi dengan standar C++23
g++ -std=c++23 main.cpp -o main
# jalankan program
./main1/2 < 1/3 : false
1/3 < 1/2 : true
1/2 > 1/3 : true
1/2 == 2/4 : true
1/2 != 1/3 : trueKeluaran menunjukkan bahwa <, >, ==, dan != semuanya bekerja tanpa satu pun definisi manual untuk operator selain <=> dan ==. Ekspresi a > b yang tampak pada program dipenuhi oleh compiler melalui hasil a <=> b yang dibandingkan dengan nol.
Kasus Pecahan juga menunjukkan bahwa operator<=> tidak selalu dapat didefaultkan begitu saja. Kelas dengan urutan member yang alami justru mendapat keuntungan penuh, misalnya Titik dengan member x dan y. Untuk kelas semacam itu, satu baris berikut sudah cukup dan seluruh perbandingannya selesai.
// tidak-dijalankan
auto operator<=>(const Titik&) const = default;Urutan member menentukan urutan nilai pada kelas semacam itu, sehingga definisi manual tidak diperlukan sama sekali. Adapun Pecahan harus menuliskan perkalian silangnya sendiri karena urutan membernya tidak mewakili urutan nilai pecahan.
Perbandingan penulisan C++17 dan C++20#
Perbedaan kedua pendekatan paling jelas pada jumlah definisi yang ditulis manual. Ringkasannya seperti berikut.
| Aspek | C++17 | C++20 |
|---|---|---|
| Operator yang ditulis manual | enam, atau dua fondasi ditambah empat baris turunan | dua, yaitu <=> dan == yang didefaultkan |
| Operator yang disintesis compiler | tidak ada | !=, <, >, <=, dan >= |
| Tipe balik perbandingan | bool | std::strong_ordering dan kawannya |
Menulis enam definisi secara manual membuka peluang inkonsistensi, misalnya == yang tidak sejalan dengan <. Pada C++20, kelima operator turunan disusun compiler dari satu definisi yang sama, sehingga inkonsistensi tersebut tidak mungkin terjadi. Sisa pekerjaan manual pun turun dari enam menjadi dua.
Kedua versi program pada topik ini menghasilkan keputusan perbandingan yang sama untuk kelima kasus ujinya. Perbedaannya murni pada siapa yang menuliskan operator turunannya, yaitu penulis kelas pada C++17 dan compiler pada C++20. Semakin banyak perbandingan yang didelegasikan kepada compiler, semakin sedikit pula peluang ketidakkonsistenan yang lolos ke dalam kode.
Kesalahan umum#
- Mendefinisikan
<tanpa==atau sebaliknya. Algoritma pustaka standar memakai keduanya, misalnya pengurutan yang membutuhkan<dan pencarian yang membutuhkan==. Tuliskan keduanya, atau cukup<=>dan==yang didefaultkan pada C++20. - Menulis
==dan<yang tidak konsisten, misalnya==membandingkan pembilang dan penyebut persis, sedangkan<memakai perkalian silang. Akibatnyaa == bbisa bernilai salah sementara!(a < b) && !(b < a)bernilai benar. Jadikan satu definisi sebagai sumber kebenaran bagi yang lain. - Memakai perkalian silang tanpa memperhitungkan rentang tipe. Hasil kali dua
intdapat melebihi batasnya untuk pecahan bernilai besar, sehingga perbandingan menjadi keliru. Simpan hasil kalinya pada tipe yang lebih lebar sepertilong long.
Ringkasan#
- Operator aritmetika mengembalikan objek baru dan ditulis sebagai free function agar kedua operand setara.
- Pada C++17,
==dan<ditulis manual, sedangkan!=,>,<=, dan>=diturunkan dari keduanya. - C++20 menggantikannya dengan satu
operator<=>danoperator==yang didefaultkan, sehingga lima operator lainnya disintesis compiler. operator==yang didefaultkan membandingkan member demi member, sehingga benar hanya bila representasi objeknya kanonis.
Referensi#
- 21.7 — Overloading the comparison operators — LearnCpp
— penulisan
==dan<beserta spaceship operator dari C++20. - Default comparisons — cppreference
— aturan operator
==dan<=>yang didefaultkan beserta penurunan operator lainnya. - std::strong_ordering — cppreference
— kategori urutan yang dikembalikan oleh
<=>untuk tipe yang selalu dapat diperbandingkan.