Virtual function di class dasar adalah undangan; overriding adalah jawabannya — class turunan menulis versi sendiri dari method tersebut, dan versi itulah yang dijalankan saat object dipanggil lewat pointer atau referensi dasar. Mekanisme ini terlihat sepele, tetapi ia penuh jebakan kecil yang baru ketahuan saat runtime: salah satu karakter di nama method, const yang lupa, atau parameter yang sedikit berbeda — semuanya bukan overriding, melainkan method baru yang membungkam versi dasar. C++ modern memberi kita dua kata kunci untuk mengunci kebenarannya: override yang memaksa compiler memeriksa, dan final yang menyatakan “sampai di sini, tidak ada yang boleh menulis ulang lagi”. Chapter ini membedah keduanya serta bedah tuntas masalah hiding.

Aturan Override: Signature Harus Sama Persis #

Versi turunan disebut melakukan overriding hanya bila signature-nya identik dengan versi dasar: nama method, daftar parameter, dan qualifier const — semuanya harus cocok. Return type juga wajib sama atau kompatibel (covariant, topik lanjutan yang jarang dibutuhkan di awal). Bila cocok dan method dasar virtual, versi turunan menggantikan versi dasar pada dynamic dispatch; virtualitas juga otomatis mengalir ke turunan tanpa menulis ulang virtual.

Satu-satunya detail yang boleh berbeda adalah isi badan method. Signature adalah kontrak yang tidak bisa ditawar — dan di sinilah kesalahan paling umum menunggu: void bersuara() yang lupa const berbeda dari void bersuara() const. Keduanya tampak serupa bagi mata manusia, tetapi bagi compiler itu dua method berbeda. Tanpa alat pemeriksa, salah satu bentuk ini lolos kompilasi dan diam-diam tidak pernah ikut dynamic dispatch.

override: Biarkan Compiler Memeriksa #

Keyword override ditulis di akhir deklarasi method turunan sebagai pernyataan tegas: “method ini dimaksudkan untuk menulis ulang virtual function”. Compiler kemudian memverifikasi klaim itu terhadap class dasar — dan bila ternyata tidak ada yang ditulis ulang, kompilasi gagal seketika. Inilah nilainya: kesalahan yang tadinya bersembunyi sampai runtime kini ditangkap di waktu kompilasi, tepat di baris yang salah.

Lihat dua kesalahan klasik yang keduanya tertangkap oleh override. Pertama, salah ketik nama — kita menulis bersuaraa dengan dua a:

text Error kompilasi: typo tertangkap oleh override
error: 'void Kucing::bersuaraa() const' marked 'override', but does not override
    void bersuaraa() const override {}
         ^~~~~~~~~

Kedua, const yang hilang — signature berbeda, sehingga bukan overriding:

text Error kompilasi: const hilang tertangkap
error: 'void Kucing::bersuara()' marked 'override', but does not override
    void bersuara() override {}
         ^~~~~~~~

Tanpa override, kedua program itu lolos kompilasi tanpa suara — dan setiap panggilan lewat referensi dasar diam-diam menjalankan versi dasar. Dengan override, keduanya berhenti di baris yang tepat. Karena itu kebiasaan modern yang disarankan: setiap method turunan yang memang menulis ulang virtual function wajib ditandai override.

Contoh Lengkap: Override Berlapis #

Program berikut memakai override di setiap tingkat hierarki, plus final pada class agar rantai inheritance berhenti di tempat yang kita pilih:

cpp
#include <iostream>

class Hewan {
public:
    virtual void bersuara() const {
        std::cout << "Hewan mengeluarkan suara\n";
    }
    virtual void makan() const {
        std::cout << "Hewan makan\n";
    }
};

class Kucing : public Hewan {
public:
    void bersuara() const override {    // override: minta compiler memeriksa
        std::cout << "Meong\n";
    }
    void makan() const override {
        std::cout << "Kucing makan ikan\n";
    }
};

class KucingAnggora final : public Kucing {   // final pada class: tidak boleh diwarisi lagi
public:
    void bersuara() const override {
        std::cout << "Meooong~\n";
    }
};

int main() {
    KucingAnggora anggora;
    const Hewan& h = anggora;    // lewat referensi dasar
    h.bersuara();                // versi paling turunan yang menang
    h.makan();                   // versi Kucing (dari class nenek moyang)
    return 0;
}

Output yang Dihasilkan:

text
Meooong~
Kucing makan ikan

Mari kita bedah:

  1. Baris 15 dan 18 (override di Kucing): kedua method menulis ulang virtual function dari Hewan. Penanda override memastikan signature-nya benar-benar cocok — bila kita lupa const di salah satunya, compiler berhenti di baris itu.
  2. Baris 23 (KucingAnggora final : public Kucing): final yang ditempel pada class menyatakan tidak ada class lain boleh mewarisi KucingAnggora. Percobaan class KucingPersia : public KucingAnggora ditolak compiler: error: cannot derive from 'final' base 'KucingAnggora' in derived type 'KucingPersia'.
  3. Baris 25 (override di KucingAnggora): class paling turunan ikut menulis ulang bersuara(). Dynamic dispatch memilih versi paling turunan yang tersedia — bukan versi dasar, bukan pula versi tingkat tengah.
  4. Baris 32-34 (referensi dasar): h bertipe const Hewan&, tetapi pemanggilannya menjawab versi KucingAnggora untuk bersuara() dan versi Kucing untuk makan(). Dua virtual function yang berbeda rantai override-nya bekerja independen — itulah dinamika yang bisa kita andalkan.

final juga bisa ditempel pada method, bukan hanya class: void bersuara() const override final; di Kucing membuat semua turunan di bawahnya dilarang menulis ulang method itu. Bila KucingAnggora mencoba, compiler menolaknya:

text Error kompilasi: method dinyatakan final
error: virtual function 'virtual void KucingAnggora::bersuara() const' overriding final function
note: overridden function is 'virtual void Kucing::bersuara() const'

Hiding: Ketika Bukan Overriding #

Signature yang tidak cocok tidak menimbulkan error — justru itu jebakannya. Method turunan dengan nama sama tetapi signature berbeda melakukan hiding: ia menyembunyikan seluruh versi dasar dari pandangan object turunan, tanpa ikut dynamic dispatch sama sekali. Program berikut sengaja memperlihatkan celah itu (const sengaja dihilangkan) — compiler g++ 14.2 memberi peringatan yang justru layak kita kutip sebagai bukti:

cpp
#include <iostream>

class Hewan {
public:
    virtual void bersuara() const {
        std::cout << "Hewan mengeluarkan suara\n";
    }
};

class Kucing : public Hewan {
public:
    // Signature BERBEDA (const hilang): hiding, bukan overriding
    void bersuara() {
        std::cout << "Meong versi non-const\n";
    }
};

int main() {
    Kucing kucing;
    kucing.bersuara();         // object non-const: versi Kucing terpilih

    const Hewan& h = kucing;   // lewat referensi dasar yang const
    h.bersuara();              // signature tidak cocok: versi Hewan terpilih
    return 0;
}

Output yang Dihasilkan:

text
Meong versi non-const
Hewan mengeluarkan suara

Baris pertama output menunjukkan kucing.bersuara() memilih versi non-const milik Kucing; baris kedua menunjukkan panggilan lewat const Hewan& menjatuhkan pilihan ke versi Hewan — dua method yang tidak pernah bertemu. Compiler memperingatkan kejadian ini bila flag peringatannya aktif:

text Peringatan compiler: virtual function disembunyikan
warning: 'virtual void Hewan::bersuara() const' was hidden [-Woverloaded-virtual=]
note:   by 'void Kucing::bersuara()'

Pola penutupnya sederhana namun kuat: nama sama tanpa override = dua dunia terpisah; nama sama dengan override = satu rantai dynamic dispatch yang jujur. Dengan alat pemeriksa ini di tangan, kita siap melangkah ke tujuan besar seluruh mekanisme: polymorphism, satu antarmuka yang melayani banyak perilaku sekaligus.

Rujukan resmi: Override specifier (cppreference) , Final specifier (cppreference) , Virtual specifier (cppreference) .