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.

OperatorBentuk yang dipilihAlasan
=, [], (), ->Wajib member functionBahasa tidak mengizinkan bentuk non-member
+=, -=, ++, --Member functionMengubah object pemanggil; sisi kiri pasti tipe class kita
+, -, *, /, ==, !=Free functionKedua operand diperlakukan simetris
<<, >> untuk streamFree 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 <<:

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

text
a + b = (4, 3)
a setelah a += b = (4, 3)
a == c? ya
a != c? tidak
d = (6, 2)

Bedah bagian pentingnya:

  1. Baris 12-16 (operator+=): member function mengubah x dan y milik object pemanggil, lalu mengembalikan *this dengan tipe Vektor2D&. Tipe referensi inilah yang membuat hasilnya bisa dilanjutkan, terbukti pada baris 61.
  2. Baris 26-29 (operator+): parameter pertama diterima by value sehingga compiler menyalin operand kiri ke salinan kerja kiri; fungsi mengubah salinan itu lewat +=, lalu mengembalikannya. Operand asli tidak tersentuh — persis perilaku a + b pada int.
  3. Baris 32-34 (operator==): kedua operand diterima sebagai const& dan diperlakukan setara — tidak ada yang istimewa karena menjadi sisi kiri.
  4. 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.
  5. Baris 51 (Vektor2D c = a + b;): compiler mencocokkan a + b dengan operator+(a, b), menghasilkan (4, 3).
  6. Baris 54 (a += b;): versi member dieksekusi dan a benar-benar berubah — kontras dengan a + b yang menghasilkan object baru tanpa menyentuh a.

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:

cpp Pola umum: operator<< untuk stream output
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:

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

text
suhu jam 3: 30 derajat
suhu jam 3 setelah koreksi: 31 derajat
rata-rata: 28.9 derajat
laporan jam 3: 31 derajat

Bedah bagian pentingnya:

  1. Baris 12-14 (versi non-const): tipe kembalian double& (referensi) membuat hasil hari[3] bisa menjadi target penugasan — bukan sekadar dibaca.
  2. Baris 17-19 (versi const): signature yang sama dengan tambahan const di akhir dan tipe kembalian const double&. Compiler memilih versi ini ketika object pemanggilnya const.
  3. Baris 31 (hari[3] = 31.0;): penulisan nilai lewat referensi yang dikembalikan versi non-const; tanpa return type referensi, baris ini tidak mungkin bekerja.
  4. Baris 36-38 (loop penjumlahan): hari[i] dipanggil berulang kali untuk membaca — satu operator, dua peran, sesuai konteks pemakaiannya.
  5. Baris 41-42 (object const): laporan disalin dari hari lewat copy constructor bawaan, lalu laporan[3] hanya bisa dipakai karena versi const tersedia. Bila kita hapus versi const, kompilasi berhenti dengan pesan berikut:
text Error kompilasi: object const memanggil operator[] non-const
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.

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

text
awal: 0
setelah dua prefix: 2
postfix: nilai lama = 2, penghitung kini = 3
prefix langsung: 4
postfix langsung: 4
akhir: 5

Bedah bagian pentingnya:

  1. Baris 11-14 (prefix): nilai dinaikkan lebih dulu, lalu object pemanggil dikembalikan sebagai referensi — (++penghitung) pada baris 41 melaporkan nilai yang sudah naik.
  2. Baris 17-21 (postfix): object lama disalin 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.
  3. Baris 37-39: penghitung++ memberikan nilai lama 2 kepada nilaiLama, sementara penghitung sendiri menjadi 3 — kedua angka itu muncul berdampingan pada output.
  4. Baris 42: (penghitung++).ambilJumlah() membaca nilai kembalian postfix, yaitu nilai lama 4, meskipun penghitung sesungguhnya sudah menjadi 5 — 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:

OperatorReturn type kanonikAlasan
+, -, *, /T (by value)Menghasilkan object baru; kedua operand utuh
+=, -=, *=, /=T&Object pemanggil yang berubah; hasil bisa dilanjutkan
=T&Penugasan berantai a = b = c
++, -- prefixT&Mengembalikan nilai baru
++, -- postfixT (by value)Mengembalikan nilai lama berupa salinan
<<, >>std::ostream&, std::istream&Stream agar bisa dirangkai
==, !=, <, >boolDipakai 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 + pelangganB tidak punya makna yang bisa ditebak semua orang — fungsi bernama seperti gabungkan() 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) .