Kenapa inheritance saja tidak cukup
Di halaman sebelumnya kamu membangun hierarki class: class turunan mewarisi member dari class induknya. Tapi ada masalahnya. Saat kamu memanggil method lewat pointer class induk, C++ menentukan fungsi mana yang dijalankan berdasarkan tipe pointer-nya, bukan tipe objek yang sebenarnya. Jadi Animal* yang sebenarnya menunjuk ke Dog tetap memanggil versi method milik Animal.
Hampir tidak pernah itu yang kamu inginkan. Biasanya kamu punya kumpulan pointer class induk, masing-masing menunjuk ke objek turunan yang berbeda, dan kamu ingin setiap objek berperilaku sebagai dirinya sendiri. Virtual function mewujudkan hal itu.
Objeknya benar-benar Dog, tapi a->speak() menjalankan Animal::speak(). Karena speak tidak virtual, compiler memilih fungsinya saat kompilasi berdasarkan tipe statis Animal*. Inilah bug yang ingin diperbaiki oleh virtual function.
Membuat fungsi menjadi virtual
Tambahkan kata kunci virtual pada method di class induk. Sekarang pemanggilannya ditentukan saat runtime berdasarkan tipe objek yang sebenarnya. Ini disebut dynamic dispatch.
Satu loop pada Animal*, tiga perilaku yang berbeda. Pointer class induk "tahu" tipe sebenarnya saat runtime dan memanggil fungsi yang sesuai. Satu mekanisme inilah, satu antarmuka dengan banyak implementasi, yang dimaksud dengan polimorfisme di C++.
Perhatikan bahwa virtual cukup ditulis di deklarasi class induk; begitu sebuah fungsi virtual, ia otomatis tetap virtual di setiap class turunan. Menuliskannya lagi di class turunan itu opsional dan berlebihan.
Selalu pakai kata kunci override
Di contoh di atas, setiap method turunan ditandai override. Kodenya tetap bisa berjalan tanpa itu, tapi sebaiknya kamu menganggapnya wajib. override (C++11) meminta compiler memastikan bahwa kamu memang sedang meng-override virtual function milik class induk dengan signature yang cocok. Kalau signature-nya sedikit salah, kamu mendapat error yang jelas, bukan bug yang diam-diam.
struct Animal {
virtual void speak() const { } // note: const
};
struct Dog : Animal {
void speak() { } // NOT const - this is a NEW function, not an override!
void speak() override { } // error: 'speak' does not override - tells you immediately
};
Tanpa override, speak() yang pertama bisa dikompilasi dengan baik tapi tidak pernah dipanggil lewat Animal*, karena signature-nya berbeda dari class induk (tidak ada const). Kamu bisa menghabiskan satu sore bertanya-tanya kenapa override-mu tidak berpengaruh apa-apa. Dengan override, compiler langsung menangkap ketidakcocokan itu. Tambahkan pada setiap fungsi yang meng-override.
Pure virtual function dan abstract class
Kadang class induk tidak punya perilaku default yang masuk akal: suara apa yang dibuat oleh "Animal" yang umum? Dalam kasus itu, deklarasikan fungsinya sebagai pure virtual dengan menuliskan = 0. Fungsi itu tidak punya isi, dan class-nya berubah menjadi abstract class yang tidak bisa diinstansiasi sendiri. Class itu hanya ada untuk mendefinisikan antarmuka yang harus dipenuhi class turunan.
Setiap subclass yang konkret wajib mengimplementasikan area(), kalau tidak ia juga tetap abstract. Beginilah C++ menyatakan "interface": abstract class yang hanya berisi pure virtual function adalah padanan interface di bahasa seperti Java.
Aturan virtual destructor
Inilah jebakan yang mengenai semua orang setidaknya sekali. Saat kamu melakukan delete pada objek lewat pointer class induk, C++ memanggil destructor yang ditemukannya, dan kalau destructor itu tidak virtual, hanya destructor induk yang dijalankan. Bagian turunannya tidak pernah dihancurkan, sehingga apa pun yang dimilikinya bocor. Standar menyebut ini undefined behavior.
Solusinya cukup satu kata: jadikan destructor class induk virtual. Maka delete p menjalankan ~Derived dulu, lalu ~Base, persis seperti seharusnya.
struct Base {
virtual ~Base() { cout << "~Base\n"; } // correct
};
// now: ~Derived then ~Base
Aturan praktis: begitu sebuah class punya virtual function apa pun, beri juga virtual destructor. Kalau sebuah class dimaksudkan sebagai class induk yang dipakai lewat pointer, destructor-nya harus virtual.
Kesalahan umum dan jebakan
Ada beberapa jebakan lain yang perlu diwaspadai begitu kamu sudah nyaman dengan virtual function:
Object slicing. Kalau kamu mengirim atau menyimpan objek turunan berdasarkan nilai ke variabel bertipe induk, bagian turunannya "terpotong" dan yang tersisa hanyalah objek induk biasa. Dynamic dispatch tidak lagi mencapai override-nya. Selalu pakai pointer atau reference untuk polimorfisme:
Dog d;
Animal a = d; // SLICED: a is now just an Animal, the Dog part is gone
a.speak(); // runs Animal::speak even though virtual
Animal& ref = d; // OK: reference keeps the real type
ref.speak(); // runs Dog::speak
Jangan memanggil virtual function dari constructor atau destructor. Selama konstruksi, bagian turunannya belum ada, jadi pemanggilan virtual mengarah ke versi milik class yang sedang dibangun, bukan override milik class turunan. Jarang sekali itu yang kamu maksud.
Dynamic dispatch punya sedikit biaya. Setiap pemanggilan virtual melewati tabel tersembunyi berisi pointer fungsi ("vtable"), satu perantara untuk setiap pemanggilan. Biayanya murah, tapi tidak gratis, jadi jangan menjadikan fungsi virtual kecuali kamu memang butuh overriding.
Memanggil versi induk dengan sengaja. Di dalam sebuah override, kamu tetap bisa memanggil implementasi induk secara eksplisit dengan Base::method(). Ini berguna saat perilaku turunan memperluas, bukan menggantikan, perilaku induknya.
Berikutnya: operator overloading
Virtual function memungkinkan objekmu menyesuaikan perilakunya lewat antarmuka bersama. Halaman berikutnya menunjukkan cara menyesuaikan operator yang bekerja pada objekmu: dengan operator overloading, kamu bisa mengajari tipe buatanmu sendiri untuk merespons +, ==, <<, dan lainnya, sehingga Vector + Vector atau cout << myObject terbaca sealami pada tipe bawaan.
Pertanyaan yang sering diajukan
Apa itu virtual function di C++?
Virtual function adalah member function yang dideklarasikan dengan kata kunci virtual di class induk, sehingga saat kamu memanggilnya lewat pointer atau reference class induk, C++ menjalankan override milik class turunan, bukan versi induknya. Pemilihan saat runtime ini disebut dynamic dispatch dan merupakan fondasi polimorfisme.
Apa perbedaan virtual function dan pure virtual function?
Virtual function punya isi dan bisa di-override. Pure virtual function dideklarasikan dengan = 0 dan tidak punya isi di class induk: ia memaksa setiap class turunan yang konkret untuk menyediakan implementasinya. Class apa pun yang punya minimal satu pure virtual function adalah abstract class dan tidak bisa diinstansiasi.
Kenapa class induk butuh virtual destructor di C++?
Kalau kamu melakukan delete pada objek turunan lewat pointer class induk dan destructor induknya tidak virtual, hanya destructor induk yang dijalankan. Bagian turunannya tidak pernah dibersihkan, sehingga sumber daya bocor dan terjadi undefined behavior. Jadikan destructor setiap class yang akan dipakai secara polimorfik bersifat virtual.