Kita sebenarnya sudah lama menikmati operator yang bekerja untuk tipe buatan pustaka standar: std::string dirangkai dengan +, dua std::vector dibandingkan dengan ==. Sekarang giliran class buatan kita sendiri mendapat hak yang sama. Operator overloading memungkinkan kita memberi makna operator — +, ==, [], ++, hingga << — bagi tipe class, sehingga ekspresi seperti total = kopi + roti menyatakan niat secara langsung tanpa panggilan fungsi yang panjang. Chapter ini membedah mekanismenya, pola penulisan yang kanonik, dan batas kapan overload justru menurunkan keterbacaan.
Mekanisme Overload: Nama Fungsi operator #
Di balik setiap operator yang dioverload, sesungguhnya ada fungsi biasa dengan nama khusus: nama fungsinya adalah kata kunci operator diikuti simbolnya, misalnya operator+, operator==, atau operator[]. Ketika compiler menemukan ekspresi a + b dan salah satu operand bertipe class, compiler mencari fungsi operator+ yang cocok — sebagai member dari a (bentuk a.operator+(b)) atau sebagai free function (bentuk operator+(a, b)), lalu memanggilnya.
Bahasa memberi sejumlah batasan agar operator hasil overload tetap mudah diprediksi:
- Minimal satu operand harus bertipe class atau enum — kita tidak bisa mengubah makna
int + int. - Tidak bisa menciptakan simbol baru;
**atau<>tetap bukan operator C++. - Precedence, associativity, dan jumlah operand tidak bisa diubah:
*selalu lebih dulu daripada+, dan+selalu memerlukan dua operand. - Empat operator tidak bisa dioverload sama sekali:
::(scope resolution),.(member access),.*(member access lewat pointer-to-member), dan?:(ternary).
Member Function atau Free Function #
Pertanyaan desain pertama: operator ditulis sebagai member function di dalam class, atau sebagai free function di luar class? Jawabannya ditentukan dua aturan. Pertama, bahasa menuntut empat operator ini wajib menjadi member function: = (assignment), [] (subscript), () (function call), dan -> (member access). Kedua, operator yang kedua operandnya selayaknya setara lebih pas ditulis sebagai free function, karena member function selalu menjadikan object di sisi kiri sebagai pemanggil.
| Operator | Bentuk yang dipilih | Alasan |
|---|---|---|
=, [], (), -> | Wajib member function | Bahasa tidak mengizinkan bentuk non-member |
+=, -=, ++, -- | Member function | Mengubah object pemanggil; sisi kiri pasti tipe class kita |
+, -, *, /, ==, != | Free function | Kedua operand diperlakukan simetris |
<<, >> untuk stream | Free function (+ friend) | Operand kiri adalah std::ostream/std::istream, bukan class kita |
Pada tabel di atas, friend adalah deklarasi yang kita pelajari pada Chapter Friend Functions dan Friend Classes: izin bagi free function untuk membaca member private, dan overload operator adalah salah satu pemakaiannya yang paling lazim.
Operator Aritmetika dan Compound Assignment #
Pola penulisan yang kanonik di ekosistem C++: tulis compound assignment (+=) sebagai member function yang benar-benar bekerja, lalu bangun operator+ sebagai free function yang memakai ulang +=. Dengan begitu logika penjumlahan hanya ada satu tempat dan kedua operator tidak mungkin berbeda perilaku. Program berikut menerapkan pola itu pada class Vektor2D, sekaligus menambahkan ==, !=, dan <<:
#include <iostream>
class Vektor2D {
private:
int x;
int y;
public:
Vektor2D(int xAwal, int yAwal) : x(xAwal), y(yAwal) {}
// Member function: operator yang mengubah object pemanggil
Vektor2D& operator+=(const Vektor2D& lain) {
x += lain.x;
y += lain.y;
return *this; // kembalikan object pemanggil agar bisa dirangkai
}
int ambilX() const { return x; }
int ambilY() const { return y; }
// friend: operator<< boleh membaca x dan y yang private
friend std::ostream& operator<<(std::ostream& os, const Vektor2D& v);
};
// Free function: operator+ dibangun di atas operator+=
Vektor2D operator+(Vektor2D kiri, const Vektor2D& kanan) {
kiri += kanan; // pakai ulang versi compound
return kiri;
}
// Free function: kedua operand diperlakukan simetris
bool operator==(const Vektor2D& kiri, const Vektor2D& kanan) {
return kiri.ambilX() == kanan.ambilX() && kiri.ambilY() == kanan.ambilY();
}
// Free function: kebalikan ==, ditulis memakai ==
bool operator!=(const Vektor2D& kiri, const Vektor2D& kanan) {
return !(kiri == kanan);
}
// Free function friend: operand kiri adalah std::ostream, bukan Vektor2D
std::ostream& operator<<(std::ostream& os, const Vektor2D& v) {
os << "(" << v.x << ", " << v.y << ")";
return os; // kembalikan stream agar << bisa dirangkai
}
int main() {
Vektor2D a(1, 2);
Vektor2D b(3, 1);
Vektor2D c = a + b; // operator+ free function
std::cout << "a + b = " << c << "\n";
a += b; // operator+= member function: a berubah
std::cout << "a setelah a += b = " << a << "\n";
std::cout << "a == c? " << (a == c ? "ya" : "tidak") << "\n";
std::cout << "a != c? " << (a != c ? "ya" : "tidak") << "\n";
Vektor2D d(0, 0);
(d += b) += b; // += mengembalikan referensi ke d, sehingga bisa dilanjutkan
std::cout << "d = " << d << "\n";
return 0;
}Output yang Dihasilkan:
a + b = (4, 3)
a setelah a += b = (4, 3)
a == c? ya
a != c? tidak
d = (6, 2)Bedah bagian pentingnya:
- Baris 12-16 (
operator+=): member function mengubahxdanymilik object pemanggil, lalu mengembalikan*thisdengan tipeVektor2D&. Tipe referensi inilah yang membuat hasilnya bisa dilanjutkan, terbukti pada baris 61. - Baris 26-29 (
operator+): parameter pertama diterima by value sehingga compiler menyalin operand kiri ke salinan kerjakiri; fungsi mengubah salinan itu lewat+=, lalu mengembalikannya. Operand asli tidak tersentuh — persis perilakua + bpadaint. - Baris 32-34 (
operator==): kedua operand diterima sebagaiconst&dan diperlakukan setara — tidak ada yang istimewa karena menjadi sisi kiri. - Baris 37-39 (
operator!=): ditulis memakai==. Pola ini menjamin!=selalu merupakan kebalikan tepat dari==; dua fungsi yang ditulis terpisah dengan logika berbeda adalah sumber kejutan. - Baris 51 (
Vektor2D c = a + b;): compiler mencocokkana + bdenganoperator+(a, b), menghasilkan(4, 3). - Baris 54 (
a += b;): versi member dieksekusi danabenar-benar berubah — kontras dengana + byang menghasilkan object baru tanpa menyentuha.
Operator « sebagai friend Function #
Ekspresi std::cout << v memiliki operand kiri bertipe std::ostream — class milik pustaka standar yang tidak mungkin kita rombak untuk menambah member function baru. Maka operator<< untuk class kita harus ditulis sebagai free function, dengan std::ostream& sebagai parameter pertama dan object kita sebagai parameter kedua. Bila fungsi itu perlu membaca member private, class menjadikannya friend — persis seperti baris 22 pada vektor2d.cpp, yang kemudian memungkinkan baris 43 membaca v.x dan v.y secara langsung.
Tipe kembalian std::ostream& bukan detail kecil: operator mengembalikan stream yang sama, sehingga rangkaian std::cout << v << "\n" bekerja — setiap << menerima stream hasil << sebelumnya. Mekanisme rangkaian ini sama dengan std::cout << a << b yang kita amati pada Chapter this Pointer. Pola umumnya bisa dihafal:
std::ostream& operator<<(std::ostream& os, const TipeKita& obj) {
os << /* tulis isi obj ke stream */;
return os; // kembalikan stream yang sama agar << bisa dirangkai
}Operator [] untuk Akses Elemen #
operator[] wajib ditulis sebagai member function, dan class yang menyediakan akses baca-tulis lazim menyiapkan dua versinya: versi non-const yang mengembalikan referensi (bisa dibaca dan ditulis) serta versi const untuk object yang bersifat const — kemampuan yang kita pelajari pada Chapter Const Member Functions. Class SuhuHarian berikut membungkus std::vector<double> dengan akses per indeks:
#include <iostream>
#include <vector>
class SuhuHarian {
private:
std::vector<double> suhu; // satu nilai suhu per jam, indeks 0..4
public:
SuhuHarian(const std::vector<double>& dataAwal) : suhu(dataAwal) {}
// Versi non-const: dipakai untuk membaca sekaligus menulis
double& operator[](int jam) {
return suhu[jam];
}
// Versi const: satu-satunya yang bisa dipakai object const
const double& operator[](int jam) const {
return suhu[jam];
}
int jumlahData() const {
return static_cast<int>(suhu.size());
}
};
int main() {
SuhuHarian hari({27.5, 28.0, 29.5, 30.0, 28.5});
std::cout << "suhu jam 3: " << hari[3] << " derajat\n";
hari[3] = 31.0; // menulis lewat operator[] versi non-const
std::cout << "suhu jam 3 setelah koreksi: " << hari[3] << " derajat\n";
double total = 0.0;
for (int i = 0; i < hari.jumlahData(); ++i) {
total += hari[i];
}
std::cout << "rata-rata: " << total / hari.jumlahData() << " derajat\n";
const SuhuHarian laporan = hari; // object const: hanya versi const yang berlaku
std::cout << "laporan jam 3: " << laporan[3] << " derajat\n";
return 0;
}Output yang Dihasilkan:
suhu jam 3: 30 derajat
suhu jam 3 setelah koreksi: 31 derajat
rata-rata: 28.9 derajat
laporan jam 3: 31 derajatBedah bagian pentingnya:
- Baris 12-14 (versi non-const): tipe kembalian
double&(referensi) membuat hasilhari[3]bisa menjadi target penugasan — bukan sekadar dibaca. - Baris 17-19 (versi const): signature yang sama dengan tambahan
constdi akhir dan tipe kembalianconst double&. Compiler memilih versi ini ketika object pemanggilnya const. - Baris 31 (
hari[3] = 31.0;): penulisan nilai lewat referensi yang dikembalikan versi non-const; tanpa return type referensi, baris ini tidak mungkin bekerja. - Baris 36-38 (loop penjumlahan):
hari[i]dipanggil berulang kali untuk membaca — satu operator, dua peran, sesuai konteks pemakaiannya. - Baris 41-42 (object const):
laporandisalin dariharilewat copy constructor bawaan, lalulaporan[3]hanya bisa dipakai karena versi const tersedia. Bila kita hapus versi const, kompilasi berhenti dengan pesan berikut:
error: passing 'const SuhuHarian' as 'this' argument discards qualifiers [-fpermissive]
note: in call to 'double& SuhuHarian::operator[](int)'Operator ++: Prefix dan Postfix #
Satu operator ++ memiliki dua bentuk: prefix (++x) dan postfix (x++). Untuk membedakannya dalam deklarasi, versi postfix menerima satu parameter dummy bertipe int — parameternya tidak pernah dipakai, hanya penanda bentuk. Keduanya punya kontrak yang berbeda: prefix mengembalikan nilai baru lewat referensi, postfix mengembalikan nilai lama lewat salinan.
#include <iostream>
class PenghitungPengunjung {
private:
int jumlah;
public:
PenghitungPengunjung() : jumlah(0) {}
// Prefix: tambah dulu, lalu kembalikan object pemanggil (by reference)
PenghitungPengunjung& operator++() {
++jumlah;
return *this;
}
// Postfix: parameter int hanyalah penanda, nilainya tidak pernah dipakai
PenghitungPengunjung operator++(int) {
PenghitungPengunjung lama = *this; // simpan nilai sebelum naik
++jumlah; // naikkan nilai sekarang
return lama; // kembalikan nilai lama (by value)
}
int ambilJumlah() const {
return jumlah;
}
};
int main() {
PenghitungPengunjung penghitung;
std::cout << "awal: " << penghitung.ambilJumlah() << "\n";
++penghitung; // prefix
++penghitung;
std::cout << "setelah dua prefix: " << penghitung.ambilJumlah() << "\n";
PenghitungPengunjung nilaiLama = penghitung++; // postfix
std::cout << "postfix: nilai lama = " << nilaiLama.ambilJumlah()
<< ", penghitung kini = " << penghitung.ambilJumlah() << "\n";
std::cout << "prefix langsung: " << (++penghitung).ambilJumlah() << "\n";
std::cout << "postfix langsung: " << (penghitung++).ambilJumlah() << "\n";
std::cout << "akhir: " << penghitung.ambilJumlah() << "\n";
return 0;
}Output yang Dihasilkan:
awal: 0
setelah dua prefix: 2
postfix: nilai lama = 2, penghitung kini = 3
prefix langsung: 4
postfix langsung: 4
akhir: 5Bedah bagian pentingnya:
- Baris 11-14 (prefix): nilai dinaikkan lebih dulu, lalu object pemanggil dikembalikan sebagai referensi —
(++penghitung)pada baris 41 melaporkan nilai yang sudah naik. - Baris 17-21 (postfix): object
lamadisalin dari keadaan sebelum naik (memakai copy constructor bawaan yang kita pelajari pada Chapter Copy Constructor dan Copy Assignment), nilai sekarang dinaikkan, dan salinan lama dikembalikan by value. - Baris 37-39:
penghitung++memberikan nilai lama2kepadanilaiLama, sementarapenghitungsendiri menjadi3— kedua angka itu muncul berdampingan pada output. - Baris 42:
(penghitung++).ambilJumlah()membaca nilai kembalian postfix, yaitu nilai lama4, meskipunpenghitungsesungguhnya sudah menjadi5— persis semantik postfix pada tipe bawaan.
Perhatikan juga bahwa postfix mengembalikan variabel lokal lama by value. Mengembalikannya sebagai referensi akan menghasilkan dangling reference — referensi ke object yang sudah berakhir — bahaya yang kita kenal dari Chapter Dangling Pointer.
Aturan Konsistensi dan Return Type #
Return type operator bebas ditentukan, tetapi ekosistem C++ menyepakati pola kanonik berikut agar operator hasil overload terasa seperti operator bawaan:
| Operator | Return type kanonik | Alasan |
|---|---|---|
+, -, *, / | T (by value) | Menghasilkan object baru; kedua operand utuh |
+=, -=, *=, /= | T& | Object pemanggil yang berubah; hasil bisa dilanjutkan |
= | T& | Penugasan berantai a = b = c |
++, -- prefix | T& | Mengembalikan nilai baru |
++, -- postfix | T (by value) | Mengembalikan nilai lama berupa salinan |
<<, >> | std::ostream&, std::istream& | Stream agar bisa dirangkai |
==, !=, <, > | bool | Dipakai dalam ekspresi kondisi |
Di luar return type, konsistensi berarti operator yang berkaitan berperilaku seragam: + dan += melakukan penjumlahan yang sama, != adalah kebalikan tepat ==, dan tidak ada operator yang memunculkan side effect yang tidak kita duga dari simbolnya.
Kapan Sebaiknya Tidak Overload #
Fasilitas ini ada untuk keterbacaan, bukan sekadar sintaks ringkas. Tunda overload bila salah satu dari ini berlaku:
- Tidak ada padanan yang disepakati:
pelangganA + pelangganBtidak punya makna yang bisa ditebak semua orang — fungsi bernama sepertigabungkan()lebih jujur tentang apa yang dilakukannya. - Maknanya ganda: untuk satu class, “penjumlahan” bisa berarti menggabungkan, menambah stok, atau menyatukan jadwal; bila butuh penjelasan panjang setiap kali dipakai, pilih nama fungsi yang eksplisit.
- Operator butuh side effect: simbol aritmetika membawa janji “hasil baru, operand utuh”; operasi yang mengubah file atau jaringan sebaiknya tidak menyembunyikannya di balik
+. - Hanya dipakai sekali di satu tempat: fungsi bernama tanpa overload lebih murah dirawat daripada antarmuka operator yang harus dijaga konsistensinya selamanya.
Dengan operator overloading, class kita kini tampil layaknya tipe bawaan: bisa dijumlah, dibandingkan, dicetak ke stream, dan diakses per elemen. Semua kemudahan itu tampil di antarmuka, sementara detailnya — member private, helper internal, resource yang dikelola — tinggal di dalam. Chapter berikutnya, Encapsulation, merumuskan pembagian tersebut menjadi prinsip desain: sembunyikan detail internal, buka hanya antarmuka yang bermakna.
Rujukan resmi: Operator overloading (cppreference) .